How to Secure Your MCP Server's API Keys (With Working Demo)

Published: (February 15, 2026 at 01:07 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

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:

flowchart LR
    A[Claude Desktop] --> B[MCP Protocol] --> C[Your Server] --> D[Janee Vault] --> E[API Call]
    D -->|AES-256-GCM encrypted| C

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");

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"
      }
    }
  }
}

One password protects all your API keys. No more scatter‑shot env vars.

Why Not Just Use .env Files?

Feature.env filesJanee vault
StoragePlaintextAES‑256‑GCM encrypted
Access controlNoneSession‑based with TTL
RotationManual find‑and‑replacejanee add service (overwrites)
AuditNoneBuilt‑in logging
Git safetyNeeds .gitignore disciplineVault 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:
  • Janee (the secrets manager):
  • MCP spec:

If you’re building MCP servers, give your secrets the respect they deserve. ⭐ Janee if it’s useful.

0 views
Back to Blog

Related posts

Read more »

dlt MCP Server for Popular IDEs

Overview This demo showcases how to set up and use the dlt MCP Server for data pipeline validation and inspection. The MCP server enables interactive querying...