The Problem Every MCP Developer Ignores
You build an MCP server. It needs a GitHub token. Maybe an OpenAI key. Where do they go?
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["server.js"],
"env": {
"GITHUB_TOKEN": "ghp_XXXXXXXXXXXX",
"OPENAI_API_KEY": "sk-XXXXXXXXXXXX"
}
}
}
}
Plaintext. In a JSON file. On disk. Possibly committed to git.
This is how most MCP servers handle secrets today. And it's a ticking time bomb.
A Better Way: Encrypted Vault + Runtime Decryption
I built a demo MCP server that does it differently. Instead of reading secrets from env vars, it pulls them from Janee — an encrypted vault designed for MCP servers.
The flow:
Claude Desktop → MCP Protocol → Your Server → Janee Vault → API Call
↑
AES-256-GCM encrypted
Decrypted only at runtime
No plaintext secrets on disk. Ever.
Try It Yourself (5 minutes)
git clone https://github.com/lucamorettibuilds/janee-mcp-demo
cd janee-mcp-demo
npm install
# Set up the encrypted vault
npx janee init # Creates encrypted vault
npx janee add github # Stores your GitHub token (encrypted)
npx janee add openai # Stores your OpenAI key (encrypted)
# Run it
export JANEE_MASTER_PASSWORD="your-password"
npm start
The server exposes three MCP tools:
-
github_create_issue— creates GitHub issues using vault credentials -
openai_complete— calls OpenAI using vault credentials -
list_secrets— shows what's in the vault (names only, never values)
How the Code Works
The secret sauce is a tiny integration layer (src/secrets.js):
import { execSync } from "child_process";
export async function getSecret(service, field) {
try {
const result = execSync(
`npx janee get ${service} ${field} 2>/dev/null`,
{ encoding: "utf-8", env: { ...process.env } }
).trim();
return result || null;
} catch {
return null;
}
}
Then in your MCP tool handler, instead of process.env.GITHUB_TOKEN:
const token = await getSecret("github", "token");
That's it. The secret is decrypted from the vault at runtime, used for the API call, and never persisted in memory longer than needed.
Claude Desktop Config
{
"mcpServers": {
"janee-demo": {
"command": "node",
"args": ["/path/to/janee-mcp-demo/src/server.js"],
"env": {
"JANEE_MASTER_PASSWORD": "your-master-password"
}
}
}
}
Notice: one password protects all your API keys. No more scatter-shot env vars.
Why Not Just Use .env Files?
| .env files | Janee vault | |
|---|---|---|
| Storage | Plaintext | AES-256-GCM encrypted |
| Access control | None | Session-based with TTL |
| Rotation | Manual find-and-replace |
janee add service (overwrites) |
| Audit | None | Built-in logging |
| Git safety | Needs .gitignore discipline | Vault file is safe to commit |
Adding Your Own Services
The demo ships with GitHub and OpenAI, but you can add anything:
npx janee add stripe
npx janee add anthropic
npx janee add postgres
Then use getSecret("stripe", "api_key") in your tool handler.
Get the Code
- Demo repo: github.com/lucamorettibuilds/janee-mcp-demo
- Janee (the secrets manager): github.com/rsdouglas/janee
- MCP spec: modelcontextprotocol.io
If you're building MCP servers, give your secrets the respect they deserve. ⭐ Janee if it's useful.
Top comments (0)