Build a HashiCorp Vault Secrets MCP Server for Agentic Credential Management in 2026
AI agents that hardcode API keys create lateral movement risk when compromised. This FastMCP server proxies HashiCorp Vault access so agents receive short-lived, scoped credentials that auto-expire—eliminating stored secrets while maintaining audit trails for every credential access.
Deepak Bagada
CEO, SaaSNext
- Dynamic Vault credentials auto-expire in 30 minutes, eliminating persistent API keys in agent conversation context
- Policy-based access control maps agent identity to Vault policies, preventing privilege escalation across tool calls
- Every credential access, issuance, and revocation is logged to Vault's audit backend for compliance and forensics
When an AI agent stores an API key in its conversation context, that key persists across the entire session. If the agent is compromised through prompt injection, every credential in its context is exposed. HashiCorp Vault solves this by issuing dynamic, short-lived credentials on demand—but Vault's HTTP API is not MCP-native, and agents cannot negotiate Vault tokens.
This FastMCP server wraps Vault's secret engine behind MCP tool calls. Instead of storing credentials, agents call get_database_credentials or get_api_token and receive time-limited tokens that auto-expire. The server maps agent identity to Vault policies, enforcing least-privilege at the tool level. Every credential access is logged to Vault's audit backend for compliance.
Architecture
Claude / Cursor Agent
│
▼ MCP Protocol
┌───────────────────┐
│ Vault MCP Server │
│ (FastMCP + Vault) │
├───────────────────┤
│ • Token Exchange │
│ • Policy Mapping │
│ • TTL Enforcement │
│ • Audit Logging │
└────────┬──────────┘
│ HTTPS
▼
┌───────────────────┐
│ HashiCorp Vault │
│ • KV v2 │
│ • Database Engine │
│ • Transit Engine │
│ • Audit Backend │
└───────────────────┘
File Structure
vault-mcp-server/
├── src/
│ ├── server.ts # FastMCP server with Vault tools
│ ├── vault-client.ts # Vault HTTP client with policy mapping
│ ├── token-manager.ts # Short-lived token caching
│ └── audit.ts # Audit event formatter
├── policies/
│ ├── agent-readonly.hcl # Read-only agent policy
│ ├── agent-database.hcl # Database credential policy
│ └── agent-transit.hcl # Encryption/decryption policy
├── config.yaml
├── package.json
└── tsconfig.json
FastMCP Server
// src/server.ts
import { FastMCP } from "fastmcp";
import { z } from "zod";
import { VaultClient } from "./vault-client.js";
import { TokenManager } from "./token-manager.js";
import { AuditLogger } from "./audit.js";
const vaultUrl = process.env.VAULT_ADDR || "https://vault.internal:8200";
const vaultToken = process.env.VAULT_TOKEN || "";
const defaultTtl = parseInt(process.env.DEFAULT_TTL || "1800"); // 30 min
const vault = new VaultClient(vaultUrl, vaultToken);
const tokenManager = new TokenManager(defaultTtl);
const audit = new AuditLogger();
const server = new FastMCP({
name: "vault-secrets",
version: "1.0.0",
});
// Tool: Get database credentials (dynamic, short-lived)
server.tool(
"get_database_credentials",
"Retrieve short-lived database credentials from Vault",
{
database: z.enum(["postgres", "mysql", "mongodb"]).describe("Database engine"),
role: z.string().describe("Vault database role (e.g., 'readonly', 'admin')"),
ttl: z.number().optional().default(1800).describe("Credential TTL in seconds (max 3600)"),
},
async ({ database, role, ttl }) => {
const effectiveTtl = Math.min(ttl, 3600);
const agentId = "current-agent"; // Extract from MCP session
audit.log("credential_request", {
agent: agentId,
database,
role,
ttl: effectiveTtl,
});
try {
const lease = await vault.generateDynamicCredentials(
`database/creds/${database}-${role}`,
effectiveTtl
);
audit.log("credential_issued", {
agent: agentId,
database,
role,
lease_id: lease.lease_id,
expires_at: new Date(Date.now() + effectiveTtl * 1000).toISOString(),
});
return {
content: [{
type: "text",
text: JSON.stringify({
username: lease.data.username,
password: lease.data.password,
expires_in: effectiveTtl,
lease_id: lease.lease_id,
warning: "Credentials auto-expire. Revoke early with revoke_credential if no longer needed.",
}, null, 2),
}],
};
} catch (error) {
audit.log("credential_denied", { agent: agentId, database, role, error: String(error) });
return { content: [{ type: "text", text: `Access denied: ${error}` }], isError: true };
}
}
);
// Tool: Get KV secret
server.tool(
"get_secret",
"Retrieve a secret from Vault KV v2 engine",
{
path: z.string().describe("Secret path (e.g., 'apps/myapp/config')"),
key: z.string().optional().describe("Specific key within the secret (returns all if omitted)"),
},
async ({ path, key }) => {
const agentId = "current-agent";
audit.log("secret_access", { agent: agentId, path, key });
try {
const secret = await vault.readSecret(path);
const value = key ? secret.data[key] : secret.data;
return {
content: [{
type: "text",
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
}],
};
} catch (error) {
audit.log("secret_denied", { agent: agentId, path, error: String(error) });
return { content: [{ type: "text", text: `Access denied: ${error}` }], isError: true };
}
}
);
// Tool: Encrypt data via Transit engine
server.tool(
"encrypt_data",
"Encrypt sensitive data using Vault Transit engine (envelope encryption)",
{
key_name: z.string().describe("Transit key name"),
plaintext: z.string().describe("Data to encrypt (base64-encoded)"),
},
async ({ key_name, plaintext }) => {
const agentId = "current-agent";
audit.log("encrypt_request", { agent: agentId, key_name });
try {
const result = await vault.encrypt(key_name, plaintext);
return {
content: [{ type: "text", text: JSON.stringify({ ciphertext: result.ciphertext }) }],
};
} catch (error) {
return { content: [{ type: "text", text: `Encryption failed: ${error}` }], isError: true };
}
}
);
// Tool: Revoke credential early
server.tool(
"revoke_credential",
"Revoke a Vault lease before it expires",
{
lease_id: z.string().describe("Vault lease ID to revoke"),
},
async ({ lease_id }) => {
const agentId = "current-agent";
audit.log("credential_revoke", { agent: agentId, lease_id });
try {
await vault.revokeLease(lease_id);
return { content: [{ type: "text", text: `Lease ${lease_id} revoked successfully.` }] };
} catch (error) {
return { content: [{ type: "text", text: `Revoke failed: ${error}` }], isError: true };
}
}
);
server.start({ transport: "stdio" });
Vault Client
// src/vault-client.ts
import https from "https";
interface VaultLease {
lease_id: string;
lease_duration: number;
data: Record<string, string>;
}
export class VaultClient {
private addr: string;
private token: string;
private agentPolicy: string;
constructor(addr: string, token: string) {
this.addr = addr;
this.token = token;
this.agentPolicy = process.env.AGENT_VAULT_POLICY || "agent-readonly";
}
private async request(method: string, path: string, body?: any): Promise<any> {
return new Promise((resolve, reject) => {
const url = new URL(`/v1${path}`, this.addr);
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname,
method,
headers: {
"X-Vault-Token": this.token,
"Content-Type": "application/json",
},
};
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(data));
} else {
reject(new Error(`Vault ${res.statusCode}: ${data}`));
}
});
});
if (body) req.write(JSON.stringify(body));
req.end();
});
}
async generateDynamicCredentials(path: string, ttl: number): Promise<VaultLease> {
const result = await this.request("POST", `${path}/generate-lease`, {
ttl,
policies: [this.agentPolicy],
});
return result.auth || result.data;
}
async readSecret(path: string): Promise<any> {
return this.request("GET", `/secret/data/${path}`);
}
async encrypt(keyName: string, plaintext: string): Promise<any> {
return this.request("POST", `/transit/encrypt/${keyName}`, { plaintext });
}
async revokeLease(leaseId: string): Promise<void> {
await this.request("POST", "/sys/leases/revoke", { lease_id: leaseId });
}
}
Vault Policy Templates
# policies/agent-database.hcl
path "database/creds/postgres-readonly" {
capabilities = ["read"]
}
path "database/creds/mysql-readonly" {
capabilities = ["read"]
}
path "database/creds/mongodb-readonly" {
capabilities = ["read"]
}
# Deny admin roles
path "database/creds/*-admin" {
capabilities = ["deny"]
}
# policies/agent-readonly.hcl
path "secret/data/apps/*/config" {
capabilities = ["read"]
}
path "transit/encrypt/*" {
capabilities = ["update"]
}
path "transit/decrypt/*" {
capabilities = ["deny"]
}
Configuration
# config.yaml
vault:
addr: https://vault.internal:8200
auth_method: approle
role_id: ${VAULT_ROLE_ID}
secret_id: ${VAULT_SECRET_ID}
agent_policy: agent-readonly
default_ttl: 1800
max_ttl: 3600
mcp:
name: vault-secrets
transport: stdio
log_level: info
// .cursor/mcp.json
{
"mcpServers": {
"vault-secrets": {
"command": "node",
"args": ["dist/server.js"],
"env": {
"VAULT_ADDR": "https://vault.internal:8200",
"VAULT_ROLE_ID": "your-role-id",
"VAULT_SECRET_ID": "your-secret-id",
"AGENT_VAULT_POLICY": "agent-readonly"
}
}
}
}
Security Hardening Checklist
- AppRole Authentication: Never use root tokens. Create AppRole auth methods with short secret_id TTLs (5 minutes).
- Agent Policy Scoping: Map each agent identity to a Vault policy that grants only the specific secrets it needs. Use path-based restrictions.
- Lease TTL Limits: Enforce maximum TTL of 3600 seconds (1 hour) for all dynamic credentials. Shorter TTLs reduce blast radius.
- Audit Backend: Enable Vault audit logging to a write-only sink (S3 with Object Lock). Every credential access must be traceable.
- Network Isolation: Run Vault behind a mTLS reverse proxy. The MCP server should only connect to Vault over encrypted channels.
Last tested: August 2026 with TypeScript 5.5, FastMCP 1.2.0, Vault 1.17, and Node v22.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Build a MinIO Object Storage MCP Server for Agentic Document Retrieval in 2026
Next Story →Build a Privacy-Preserving Synthetic Data Generation Pipeline with LangGraph & Opacus DP-SGD in 2026
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...