Topic: mcp server access control
MCP server access control — who can call your tools and what they can do
The MCP protocol does not mandate caller authentication. A tool handler receives a JSON-encoded argument object from the host client and executes. By default, any process that can reach the server — any LLM call in the session, any plugin in the same agent loop — can invoke any tool with any argument. This absence of a built-in access control layer is the most common source of Permissions-axis findings in the SkillAudit corpus. The fixes exist and are straightforward; they simply require the author to explicitly model who the callers are and what they should be allowed to do.
Why MCP servers lack access control by default
MCP servers that run over stdio are launched as child processes of a single trusted client (Claude Code, Cursor, etc.). The implicit model is: if you can start the server, you're trusted. This works for personal-use servers where the only caller is the developer's own agent session. It breaks down in three scenarios:
- Multi-tenant deployments. A team running a shared MCP server on HTTP transport where multiple users' agent sessions can make requests. Tool handlers need to know which user is calling and enforce per-user authorization.
- Multi-tool agent loops. An agent that has multiple MCP servers registered can instruct any of them to call any of the others indirectly by passing their output as arguments. This creates cross-tool privilege chaining that no individual server sees as an access control issue.
- Prompt-injection attacks. A malicious document fetched by one tool provides instructions that the LLM follows, resulting in calls to other tools the user did not intend. Without caller identity gates, the server can't distinguish "user instructed this" from "injected instruction".
Pattern 1: missing caller identity gate on sensitive tools
The most common Permissions HIGH in the corpus is a tool that modifies persistent state — writes to a file, calls a destructive API, modifies a database record — with no check on who the caller is.
// Vulnerable: no caller gate on destructive operation
server.registerTool("delete_record", async ({ id }) => {
await db.records.delete({ where: { id } });
return { deleted: id };
});
For HTTP-transport servers, the fix is a middleware layer that reads a caller identity token from the request context and validates it before the tool handler runs:
// Fixed: extract caller identity from transport context
server.registerTool("delete_record", async ({ id }, context) => {
const caller = context.transport?.headers?.["x-mcp-caller-id"];
if (!caller) throw new Error("caller identity required");
const record = await db.records.findUnique({ where: { id } });
if (record.ownerId !== caller) throw new Error("not authorized");
await db.records.delete({ where: { id } });
return { deleted: id };
});
For stdio servers used in personal or single-user contexts, the risk is lower but not zero — prompt injection can still invoke tools the user didn't intend. A confirmation-required pattern (return a "about to delete X — confirm?" response and check for an explicit confirmation argument on the next call) adds a meaningful speed bump against injected deletions.
Pattern 2: ambient authority — tools inherit the server process's full permissions
MCP servers run as processes. Unless explicitly restricted, they inherit the full filesystem access, network access, and environment variables of the parent process. A tool that is designed to read a single config file can — through a path traversal bug or a prompt-injection attack — read any file the process can access.
The Permissions axis checks for this via two signals:
- mcp_config.json scope declarations. Does the server declare minimum viable scopes? Over-broad declarations (e.g.,
files: "*"instead offiles: "./config/*") are a WARN, not a HIGH, but they're a reliable signal of ambient-authority design. - Path constraint absence. Tool handlers that accept a file path argument with no allowlist or prefix constraint are flagged. A
readFile({ path })that passespathdirectly tofs.readFileis a path-traversal risk combined with an ambient-authority design flaw.
The fix is to declare explicit constraints in both the mcp_config.json and the tool handler itself:
// Fixed: constrain path to allowed prefix
import path from "path";
const ALLOWED_BASE = path.resolve("./workspace");
server.registerTool("read_file", async ({ filepath }) => {
const resolved = path.resolve(ALLOWED_BASE, filepath);
if (!resolved.startsWith(ALLOWED_BASE)) {
throw new Error("path outside allowed workspace");
}
return { content: await fs.readFile(resolved, "utf8") };
});
Pattern 3: cross-tool privilege chaining
This pattern is not caught by static analysis in SkillAudit's current version — it requires dynamic analysis or manual review. But it's worth understanding because it's a structural access control issue unique to MCP architectures.
Consider a server with two tools: fetch_url (reads a web page) and write_file (writes to disk). Neither tool is individually dangerous if both have appropriate constraints. But an LLM agent with both tools registered can be prompted to: fetch a URL that returns malicious content → use that content as the argument to write_file → overwrite a critical file. The MCP server sees two valid tool calls; the combined effect was not authorized.
Mitigations at the server level:
- Separate tools that read from untrusted sources and tools that write to sensitive destinations into different servers with different trust levels in the client config.
- Add a content-type constraint to write tools: if the source of the content isn't the user session (i.e., if it came from an external fetch), require an explicit user confirmation step.
- Log all tool invocations with the session identifier to detect automated chains that weren't user-initiated.
What SkillAudit checks on the Permissions axis
The Permissions axis in every SkillAudit report covers:
- mcp_config.json scope declarations vs. actual tool capabilities (over-declaration = WARN)
- Destructive tools without caller identity gates (HIGH on HTTP-transport servers)
- Path arguments without prefix constraints (WARN, higher severity if combined with Security axis findings)
- OAuth token scope minimization for external API calls (WARN if broad scopes are declared)
- Presence of audit logging for tool invocations (INFO — not scored, but surfaces the absence)
See the Methodology page for the full Permissions axis scoring rules. For the most common access control fix — path constraint patterns — the MCP server security checklist has the grep command to find unconstrained path arguments in your repo.
Check your server's Permissions score
Paste your GitHub URL to see the full Permissions axis breakdown — which scopes are over-declared, which tools are missing caller gates, and the specific file paths to fix.
Run a free audit