DEV Community

Atlas Whoff
Atlas Whoff

Posted on

How MCP Servers Handle Authentication (And Where They Get It Wrong)

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:

  1. Authenticating callers — verifying that the Claude Code session connecting to your server is authorized
  2. Authenticating to external services — securely using API keys to call third-party APIs
  3. 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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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"
});
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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 }));
Enter fullscreen mode Exit fullscreen mode

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:

  1. Does it expose an HTTP endpoint? If yes, does it require authentication to connect?
  2. Does it use any external API keys? Are they loaded from env vars, or hardcoded?
  3. Do error messages echo credentials? Search for process.env inside catch blocks.
  4. Does it cache tokens? Where? In plaintext?
  5. 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)