Dev.to Security 🔐 Cybersecurity 👁 0 📖 4 min read

Your MCP Server Checks the Token. Does It Check the Tenant?

You built an MCP server so every assistant in the company can call the same internal tools. You put a bearer token on /mcp, added a scope check, and tested it both ways: the right token works, and a missing token gets a

Your MCP Server Checks the Token. Does It Check the Tenant?

You built an MCP server so every assistant in the company can call the same internal tools. You put a bearer token on /mcp, added a scope check, and tested it both ways: the right token works, and a missing token gets a 401.

It feels finished. Here is the request it does not stop.

The request that gets through

Picture a deployment tool behind that server. It takes a service name and returns that service's recent deployments. (This scenario is illustrative.)

  1. Maya works at Acme. Her assistant connects with a valid token that carries the mcp:devops scope.
  2. She asks, "What shipped to payments last night?" The model calls list_recent_deployments with service: "payments". Fine.
  3. Later, a pasted ticket mentions payments-globex, a service that belongs to another tenant. The model calls the tool with that name.
  4. The endpoint sees a valid token with the right scope and waves it through. The tool does what it was asked. Another tenant's deployment history comes back to an Acme user.

A valid token tells you who is calling. It does not tell you what the model is allowed to ask for.

Do you have this problem?

Open your server and check:

  • A tool takes a service, tenant, project or id argument and passes it straight to a backend
  • The only authorization code sits on the endpoint (RequireAuthorization), and none inside the tools
  • You have never run a test where a valid token asks for someone else's resource
  • Some tools are "just read-only", so nobody looked at what they can leak

If you ticked even one, keep reading.

Why the demo never shows it

The endpoint policy is a coarse boundary: right issuer, right audience, right scope. In a demo, the person asking owns everything they ask for, so every request passes.

The gap only opens when the arguments point at something the caller does not own. And the model is the one choosing the arguments.

The fix: check inside the tool

Each sensitive tool derives the caller from the validated identity and authorizes the specific resource before it does anything else. Look at the first line of the method body:

[McpServerTool(Name = "list_recent_deployments")]
[Description("List recent deployments for a service")]
public async Task<string> ListDeployments(
    [Description("Service name")] string service,
    [Description("Number of deployments to return")] int count = 5)
{
    await callerAccess.RequireServicePermissionAsync(service, "deployments:read");
    var response = await _http.GetAsync(
        $"/api/deployments?service={Uri.EscapeDataString(service)}&top={count}");
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

callerAccess is built from validated claims for HTTP, and from an explicit local policy for stdio. It never trusts a tenant or service name supplied by the model.

Prove it with six tests

Write these before the second tool, not after the first incident:

  • No token
  • A token from the wrong issuer
  • A token for the wrong audience
  • An expired token
  • A valid token with a missing scope
  • A valid token asking for another tenant's resource

If that last test passes, you have a backdoor with a nice schema.

What I'd do first

Start with one read-only tool that a team actually wants, such as deployment status or scoped log search, and make it tenant-aware and tested before you add another. Read-only does not mean low risk: the risk comes from the data and permissions the tool exposes.

And if you have one assistant and one tool, a direct integration is simpler. MCP pays off once the same tool is copied across several assistants.

Go deeper

That is the permission half of running an MCP server in production. The full guide on AI Wisdom covers the rest:

  • The complete five-step .NET build: stdio, tools, VS Code mcp.json, and Streamable HTTP with JWT bearer auth
  • What the 2026-07-28 revision removed (the initialize handshake and Mcp-Session-Id), so any replica can answer a request
  • Routing and rate limiting by tool with the new Mcp-Name header
  • Long-running tools that survive dropped connections and redeploys
  • A completion checklist to run before you hand the server to another team
Full guide on AI Wisdom
AI Wisdom MCP Servers: Building Tool-Using AI Agents with the Model Context Protocol
How MCP works, how to build MCP servers that expose tools to AI agents, and practical patterns for connecting LLMs to your systems.
Read the full guide on aiwisdom.dev →

One check, right now: open your MCP server, find a tool that takes a service or tenant argument, and look at where the caller's access to that value is checked. Reply with where yours is: "endpoint only", "inside the tool", or "nowhere I can find".

The free Production AI System Checklist is a 6-page PDF with 30 checks from the same site.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.