Authentication Patterns for MCP Tools
Most MCP tools need authentication — API keys, tokens, or OAuth. This guide covers secure patterns for every scenario.
Pattern 1: Environment Variables (Most Common)
Best for API keys and static tokens.
const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) {
console.error("MY_API_KEY is required");
process.exit(1);
}
User configures in their MCP client:
{
"mcpServers": {
"my-tool": {
"command": "npx",
"args": ["-y", "my-tool"],
"env": {
"MY_API_KEY": "${MY_API_KEY}"
}
}
}
}
Pros: Simple, secure, works everywhere
Cons: Tokens are static; rotation requires config update
Pattern 2: Config File
For tools with complex configuration.
import { readFile } from "fs/promises";
import { homedir } from "os";
import { join } from "path";
async function loadConfig() {
const path = join(homedir(), ".my-tool", "config.json");
try {
return JSON.parse(await readFile(path, "utf-8"));
} catch {
console.error("Config not found. Run: my-tool setup");
process.exit(1);
}
}
const config = await loadConfig();
Pros: Supports multiple credentials, complex settings
Cons: Extra setup step; file permissions matter
Pattern 3: OAuth Device Flow
For tools that need user-specific access.
async function authenticate() {
const { device_code, user_code, verification_uri } = await fetch(
"https://auth.service.com/device/code",
{ method: "POST", body: JSON.stringify({ client_id: CLIENT_ID }) }
).then(r => r.json());
console.error(`Please visit: ${verification_uri}`);
console.error(`Enter code: ${user_code}`);
for (let i = 0; i < 30; i++) {
await sleep(2000);
const resp = await fetch("https://auth.service.com/oauth/token", {
method: "POST",
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
})
});
if (resp.ok) {
const { access_token, refresh_token } = await resp.json();
return { access_token, refresh_token };
}
}
throw new Error("Authentication timed out");
}
Pros: User-specific access, refreshable tokens
Cons: Complex implementation; requires browser interaction
Pattern 4: Token Caching
Avoid re-authenticating on every tool call:
import { writeFile, readFile } from "fs/promises";
const CACHE_PATH = join(homedir(), ".my-tool", "token.json");
async function getToken() {
try {
const cached = JSON.parse(await readFile(CACHE_PATH, "utf-8"));
if (cached.expires_at > Date.now()) {
return cached.access_token;
}
} catch {}
const token = await authenticate();
await writeFile(CACHE_PATH, JSON.stringify({
access_token: token.access_token,
expires_at: Date.now() + 3600 * 1000
}));
return token.access_token;
}
Pattern 5: CI/CD Authentication
For tools used in automated pipelines:
const TOKEN = process.env.MY_TOOL_TOKEN
|| (await loadConfig()).token;
if (!TOKEN) {
console.error(
"Authentication required. Set MY_TOOL_TOKEN env var or run setup."
);
process.exit(1);
}
Security Checklist
- ✅ Never log tokens or API keys
- ✅ Validate tokens on startup
- ✅ Handle expired tokens with clear error messages
- ✅ Use
process.exit(1) when auth fails — don't start the server
- ✅ Store cached tokens with restricted file permissions
- ✅ Support token refresh when possible
Error Messages for Auth Failures
throw new Error("401: invalid_grant");
return {
content: [{
type: "text",
text: "❌ Authentication failed. Your API key may be expired. " +
"Get a new key at https://service.com/keys and update your MCP config."
}]
};
Choose the right pattern for your tool's security needs. Start simple, add complexity only when needed.
#MCP #Tutorial #Authentication #Security #mcpm
Authentication Patterns for MCP Tools
Most MCP tools need authentication — API keys, tokens, or OAuth. This guide covers secure patterns for every scenario.
Pattern 1: Environment Variables (Most Common)
Best for API keys and static tokens.
const API_KEY = process.env.MY_API_KEY; if (!API_KEY) { console.error("MY_API_KEY is required"); process.exit(1); }User configures in their MCP client:
{ "mcpServers": { "my-tool": { "command": "npx", "args": ["-y", "my-tool"], "env": { "MY_API_KEY": "${MY_API_KEY}" } } } }Pros: Simple, secure, works everywhere
Cons: Tokens are static; rotation requires config update
Pattern 2: Config File
For tools with complex configuration.
import { readFile } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; async function loadConfig() { const path = join(homedir(), ".my-tool", "config.json"); try { return JSON.parse(await readFile(path, "utf-8")); } catch { console.error("Config not found. Run: my-tool setup"); process.exit(1); } } const config = await loadConfig();Pros: Supports multiple credentials, complex settings
Cons: Extra setup step; file permissions matter
Pattern 3: OAuth Device Flow
For tools that need user-specific access.
async function authenticate() { // Step 1: Request device code const { device_code, user_code, verification_uri } = await fetch( "https://auth.service.com/device/code", { method: "POST", body: JSON.stringify({ client_id: CLIENT_ID }) } ).then(r => r.json()); // Step 2: Tell user to authenticate console.error(`Please visit: ${verification_uri}`); console.error(`Enter code: ${user_code}`); // Step 3: Poll for token for (let i = 0; i < 30; i++) { await sleep(2000); const resp = await fetch("https://auth.service.com/oauth/token", { method: "POST", body: JSON.stringify({ client_id: CLIENT_ID, device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }) }); if (resp.ok) { const { access_token, refresh_token } = await resp.json(); return { access_token, refresh_token }; } } throw new Error("Authentication timed out"); }Pros: User-specific access, refreshable tokens
Cons: Complex implementation; requires browser interaction
Pattern 4: Token Caching
Avoid re-authenticating on every tool call:
import { writeFile, readFile } from "fs/promises"; const CACHE_PATH = join(homedir(), ".my-tool", "token.json"); async function getToken() { // Try cache first try { const cached = JSON.parse(await readFile(CACHE_PATH, "utf-8")); if (cached.expires_at > Date.now()) { return cached.access_token; } } catch {} // Get fresh token const token = await authenticate(); await writeFile(CACHE_PATH, JSON.stringify({ access_token: token.access_token, expires_at: Date.now() + 3600 * 1000 // 1 hour })); return token.access_token; }Pattern 5: CI/CD Authentication
For tools used in automated pipelines:
// Accept token via env var OR config file const TOKEN = process.env.MY_TOOL_TOKEN || (await loadConfig()).token; if (!TOKEN) { console.error( "Authentication required. Set MY_TOOL_TOKEN env var or run setup." ); process.exit(1); }Security Checklist
process.exit(1)when auth fails — don't start the serverError Messages for Auth Failures
// ❌ Bad — exposes internals throw new Error("401: invalid_grant"); // ✅ Good — actionable return { content: [{ type: "text", text: "❌ Authentication failed. Your API key may be expired. " + "Get a new key at https://service.com/keys and update your MCP config." }] };Choose the right pattern for your tool's security needs. Start simple, add complexity only when needed.
#MCP #Tutorial #Authentication #Security #mcpm