How MCP Servers Handle Authentication (And Where They Get It Wrong)
Authentication is one of the most frequently mishandled aspects of MCP server design. I've reviewed dozens of open-source servers and the same mistakes appear repeatedly.
Here's what correct MCP authentication looks like — and the patterns that create security vulnerabilities.
The Authentication Problem Space
MCP servers face three distinct authentication challenges:
- Authenticating callers — verifying that the Claude Code session connecting to your server is authorized
- Authenticating to external services — securely using API keys to call third-party APIs
- Authorizing tool calls — ensuring specific tools can only be called with sufficient permissions
Most tutorials only address #2, and often do it wrong.
Problem 1: MCP Server Has No Caller Authentication
The MCP spec doesn't mandate caller authentication. By default, any process that can reach your MCP server can call its tools.
For locally-running MCP servers (connected via stdio), this is less of a concern — only processes running as your user can connect.
For network-accessible MCP servers (HTTP transport), this is critical. Without authentication:
// Any HTTP client can call this — no auth required
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return await doSomethingPrivileged(request.params.arguments);
});
The fix for HTTP-transport servers:
import { createServer } from "http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const httpServer = createServer(async (req, res) => {
// ✅ Verify API key before processing any request
const apiKey = req.headers["x-api-key"];
if (!apiKey || apiKey !== process.env.MCP_API_KEY) {
res.writeHead(401);
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
// Handle MCP request
const transport = new StreamableHTTPServerTransport({ ... });
await server.connect(transport);
});
Problem 2: API Keys Leaked in Source
The most common vulnerability I've found: API keys hardcoded in source code.
// ❌ Found in real open-source MCP servers
const client = new SomeAPIClient({
apiKey: "sk-abc123realkey...",
endpoint: "https://api.example.com"
});
This gets committed to GitHub. Even if you delete it later, it's in git history.
Correct pattern:
// ✅ Load from environment at startup
const apiKey = process.env.SOME_SERVICE_API_KEY;
if (!apiKey) {
throw new Error("SOME_SERVICE_API_KEY is required");
}
const client = new SomeAPIClient({ apiKey });
And ensure the key is never returned in tool responses:
// ❌ Leaks the key in error messages
} catch (err) {
return { error: `Failed with key ${process.env.SOME_SERVICE_API_KEY}: ${err.message}` };
}
// ✅ Generic error, no credential data
} catch (err) {
return { error: "Service authentication failed", isError: true };
}
Problem 3: Over-Privileged Tool Permissions
Many MCP servers give every tool the same level of access. A tool that only needs to read data gets the same credentials as a tool that can write or delete.
Better pattern — scoped credentials:
// ✅ Read-only token for read operations
const readOnlyClient = new DBClient({
connectionString: process.env.DB_READ_URL
});
// ✅ Write token only used where writes are explicitly needed
const writeClient = new DBClient({
connectionString: process.env.DB_WRITE_URL
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "query_data":
return readOnlyClient.query(request.params.arguments.sql);
case "insert_record":
// Only this tool gets write access
return writeClient.insert(request.params.arguments);
}
});
If an attacker gains control of the read tool (via prompt injection), they can't write data.
Problem 4: Session Tokens Persisted Insecurely
Some MCP servers cache authentication tokens to avoid repeated logins:
// ❌ Token written to disk in plaintext
fs.writeFileSync(".mcp-session", JSON.stringify({ token: userToken }));
Anyone with file system access to the MCP server's working directory (including other MCP servers) can read this file.
Better options:
- Use OS keychain (macOS Keychain, Linux libsecret) for token storage
- Re-authenticate on each session start rather than persisting tokens
- If you must persist, use encryption with a key derived from a machine identifier
Audit Questions for Any MCP Server
Before installing a server that handles authentication:
- Does it expose an HTTP endpoint? If yes, does it require authentication to connect?
- Does it use any external API keys? Are they loaded from env vars, or hardcoded?
-
Do error messages echo credentials? Search for
process.envinside catch blocks. - Does it cache tokens? Where? In plaintext?
- Are tool permissions scoped? Or does every tool get full credentials?
Automated Checking
The MCP Security Scanner Pro checks authentication patterns including hardcoded credentials, credential leakage in error messages, and insecure token storage.
MCP Security Scanner Pro — $29
Atlas — building security-first developer tools at whoffagents.com
Top comments (0)