patchbay audit
·4 min read

Your MCP server lists its tools to anyone. When that is fine and when it is not

Send an MCP server a tools/list request with no credentials. Either it answers with 401 or it hands you the names and descriptions of everything it can do.

The second case is not always a bug. Some servers are public on purpose. The question that matters is what is on the list.

Authentication is optional in MCP

The MCP authorization spec says authorization is optional. A server that searches public documentation or returns public data has no reason to ask for a login. Anyone can read that data anyway.

That changes the moment one tool does something on behalf of an account. A tool that creates a record, sends a message or deletes a file acts with the permissions of whoever runs the server. If the server does not ask who is calling, everyone who finds the URL is that person.

Try it on your own server

curl -s -X POST https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A protected server answers with 401 and a WWW-Authenticate header that points to its resource metadata. An open server answers with a JSON list of tools. Some servers expect an initialize request first, so an error here does not prove the server is protected.

What I saw on public servers

In a sweep of 277 public MCP server URLs on 2026-11-06, 264 were reachable. 209 asked for authentication. 55 answered without it and 34 of those returned a complete tool list.

6 of the 34 had at least one tool whose name reads like a write operation. That number needs context. Three are still under manual review. One is public by design. Two were confirmed and reported to the operators, which is why no server is named here.

How the check decides

npx -y @patchbay-nebius/agent audit https://mcp.example.com/mcp

The finding is AUTH-001. It needs no flags and the browser scanner runs it too. The check asks for the tool list without an Authorization or x-api-key header and then looks at the tool names.

  • Critical if a name contains a word like create, delete, send, execute or deploy.
  • Info if every name looks like a read, for example get, list or search.
  • Info if the server publishes /.well-known/ai-catalog.json and declares there that it is public on purpose.
  • Info if the host belongs to a cloud platform that enforces its own identity layer on every call, such as Google Cloud, AWS or Azure.

What the check cannot tell you

It never calls a tool. A scanner that runs delete_user on a stranger's server to see what happens would be an attack. So the check knows that the list is open. It does not know whether a call would go through. Some servers show the list to everyone and still reject every call without a token.

It also judges by name. A tool called run_report is flagged although it only reads. A tool called sync is not flagged although it may write. Treat a critical result as a reason to look, not as proof.

The fix

  1. Decide whether the server is meant to be public. If it is, make sure no tool can write and say so in your docs.
  2. If it is not, protect tools/call first. Hiding the list while calls stay open fixes nothing.
  3. Answer every unauthenticated request with 401 and a WWW-Authenticate header. That includes initialize and tools/list.
  4. Publish /.well-known/oauth-protected-resource so clients can find your authorization server without being configured by hand.
  5. Validate every token on every request. A session that was authenticated once is not a credential.

If you connect agents to servers you do not run, the same test works from the other side. A server that hands its write tools to anyone hands them to your agent and to everyone else. The list can also change after you approved it.