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
Founder & Editor-in-Chief
- 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.
Related Architecture & Implementation Resources
- Browse complementary servers and client connectors in the Daily AI World MCP Directory.
- Integrate this tool into multi-agent pipelines with our AI Workflows Blueprints.
- Review frontier LLM capabilities and token metrics on Latest AI News.
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
Founder & Editor-in-Chief
Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.
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
Stop the Burnout: Building an AI Employee Retention Monitor Guide
Build an AI Employee Retention Monitor with FastMCP in Python. Aggregate non-invasive workload telemetries, predict burnout scores, and prevent regretted turnover.
Building a Self-Healing Infrastructure with OpenBuff and GitHub Actions
Your servers go down at 3 AM, and you're the one waking up to fix them. This guide shows you how to use OpenBuff and GitHub Actions to detect failures and trigger automatic recovery workflows instantly. Stop manual resta...
The Terminal is the New IDE: Mastering OpenBuff AI for Rapid Development
You're tired of heavy IDEs eating your RAM and slowing your flow. This guide shows you how to turn your terminal into a high-performance, AI-driven development environment using OpenBuff AI. Stop context switching and st...