fix(server-core): require auth for MCP execution routes under legacy auth
PR Checklist
Please check if your PR fulfills the following requirements:
- The commit message follows our guidelines: https://voltagent.dev/docs/community/contributing/#commit-convention
Bugs / Features
- Related issue(s) linked
- Tests for the changes have been added
- Docs have been added / updated
- Changesets have been added https://voltagent.dev/docs/community/contributing/#creating-a-changeset
What is the current behavior?
With the legacy auth option, requiresAuth() protects agent, tool and workflow execution through PROTECTED_ROUTES, but none of the MCP execution routes are in that list. They fall through to defaultPrivate ?? false, so by default an MCP client can run every tool, agent and workflow an MCP server exposes, and read its prompts and resources, without a token:
/mcp/:serverId/mcp(Streamable HTTP, GET/POST/DELETE)GET /mcp/:serverId/sseandPOST /mcp/:serverId/messages(SSE transport)POST /mcp/servers/:serverId/tools/:toolName(Hono tool invocation) andPOST /mcp/:serverId/tools/:toolName/invoke(Elysia)GET /mcp/servers/:serverId/prompts/:promptNameandGET /mcp/servers/:serverId/resources/contents(Hono),GET /mcp/:serverId/prompts/:promptNameandGET /mcp/:serverId/resources/read(Elysia): these run the server's prompt and resource handlers and return their contentPOST /mcp/servers/:serverId/logging/level(Hono) andPOST /mcp/:serverId/logging/level(Elysia)
The issue lists the transport paths as /mcp/servers/:id/...; the routes actually registered are /mcp/:serverId/{mcp,sse,messages}, and the REST routes above were open as well. authNext was not affected, since it treats every non-public, non-console route as user-protected.
What is the new behavior?
These MCP routes are added to PROTECTED_ROUTES, so legacy auth answers 401 without a valid token, the same as POST /agents/:id/text.
Discovery is unchanged. GET /mcp/servers, GET /mcp/servers/:serverId and GET /mcp/servers/:serverId/tools stay public. The prompt, resource and resource template lists (GET /mcp/servers/:serverId/{prompts,resources,resource-templates}) stay public in the default mode, and with defaultPrivate: true they require auth, as before. These lists only return names and descriptions. An app that wants open MCP access can still list a route in publicRoutes.
Tests:
server-core/src/auth/defaults.spec.ts: MCP transports, tool invocation, prompt and resource reads and logging level require auth; discovery and the prompt/resource lists stay public in the default mode; withdefaultPrivate: truediscovery stays public and the lists require auth;publicRoutescan still open a transport route.server-hono/src/auth/middleware.spec.ts: the middleware returns 401 for these MCP routes without a token and lets a valid token through.server-elysia/src/auth/middleware.spec.ts: 401 for the Elysia tool invocation, logging level, prompt and resource read routes without a token.
The new assertions fail on main and pass with the change. The legacy auth section of website/docs/api/authentication.md and a @voltagent/server-core patch changeset are included.
fixes #1493
Notes for reviewers
Checked against a real server (honoServer with legacy auth and an MCPServer exposing one tool), calls without a token. The after run also covers a prompt and a resource, which return 401 without a token and their content with one:
| Before | After |
|---|---|
![]() | ![]() |
With a valid token, a full Streamable HTTP session (initialize, notifications/initialized, tools/call) still works, and over SSE the stream opens and POST /mcp/:serverId/messages returns 202. A message without a token is rejected with 401 even when it carries a valid sessionId.
One known edge, unchanged by this PR: route matching is by segment, so for an MCP server whose id is literally servers, GET /mcp/servers/mcp and GET /mcp/servers/sse match the public GET /mcp/servers/:serverId discovery route first. POST to those paths is still protected.
Validation run locally (Node 24): pnpm build:all, pnpm test:all --concurrency 1 and pnpm lint (only the existing complexity warnings). Four things fail the same way on unchanged main in this environment and were left out of test:all: @voltagent/resumable-streams, @voltagent/voltagent-memory and @voltagent/cloudflare-d1 have no test files, so vitest stops with "No projects were found", and server-elysia/src/elysia-server-provider.spec.ts fails all 6 tests (also on Node 20). The rest of server-elysia was run separately with that one file excluded. For the e2e package, the Postgres test container was started with docker compose ... up -d --wait first.
The issue also mentions LocalSandbox isolation, prompt cache TTL 0 and prompt logging. Those are separate from the auth gap and are not touched here.
Summary by CodeRabbit
- Bug Fixes
- Legacy authentication now protects MCP execution routes, including tool calls, prompt and resource reads, and logging-level changes. Discovery routes remain public.
- The
publicRoutesoption can expose a specific MCP route without making sibling routes public.
- Documentation
- Updated authentication guidance to clarify MCP route protection under legacy authentication settings.

