Repository navigation
fix(server): read a static resource by the URI it is listed under - #3074
mkantautas wants to merge 1 commit into
Conversation
McpServer keys static resources by the string passed to registerResource and lists that string, but resources/read and the scope-challenge resolver looked the resource up by new URL(uri).toString(). A URI the WHATWG parser serializes differently (https://example.com, an uppercase host, a default port, dot segments) was listed but answered with Resource not found. Both lookups now try the requested URI first and fall back to its parsed form when no resource is registered under that exact string. A resource registered under the exact URI therefore takes precedence over a parsed-form twin or a matching template, including when it is disabled.
🦋 Changeset detectedLatest commit: a5db863 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
Read the diff; I did not run the suite. One case the new tests do not cover: the lookup is now |
McpServerlists a static resource under the string passed toregisterResource(), butresources/readlooked it up bynew URL(uri).toString(). A URI that the URL parser serializes differently is listed and then not found:The same happens for an uppercase host (
https://Example.com/docs), a default port (https://example.com:443/docs) and dot segments (file:///docs/a/../b.md).Motivation and Context
A client that reads a resource by the URI it got from
resources/listshould get that resource. The Python SDK looks up concrete resources by the exact string the request carries.What changed
resources/read, and theresources/readbranch of the scope-challenge resolver, look up the requested URI first. When no resource is registered under that exact string, they look up its parsed form as before. The scope challenge runs for the same resource the read serves.@modelcontextprotocol/server.How Has This Been Tested?
test/integration/test/server/resourceReadListedUri.test.ts: list-then-read for the four URIs above, a read by an equivalent URI that parses to the registered form, exact-key precedence whenhttps://example.comandhttps://example.com/are both registered, and a disabled exact-key resource that is rejected without reading its parsed-form twin. On main (9adb8d5) all but the parsed-form case fail; all seven pass with the fix.packages/server/test/server/scopeChallengePrimitives.test.ts: a scope challenge on a resource registered ashttps://example.comanswers 403 before the read callback runs (200 on main), and a disabledhttps://example.comresolves no challenge from an enabledhttps://example.com/twin (main resolves the twin's challenge).pnpm run check:all,build:allandscripts/smoke-dist-types.mjspass;pnpm -r --filter '!@modelcontextprotocol/test-e2e' testpasses on Node 20.20.2, 22.23.4 and 24.22.1; the Bun 1.3.13 and Deno 2.9.8 integration tests, the client and server conformance suites against their expected-failure baselines,run-examplesanddocs:examplespass on Node 24 (onerun-examplesrun had anoauth-client-credentialsleg fail with ECONNREFUSED at startup; it passed on the rerun and on main).test-e2epasses on Node 20. On Node 22 and 24 the timeout scenarios intest/e2e/scenarios/protocol.test.tsfail or leave unhandledREQUEST_TIMEOUTrejections in my containers, the same way on main (related: [v2] e2eprotocol:timeout:max-totalis flaky over SSE: fake timers advance before the request reaches the server #3045).Breaking Changes
A static resource registered under the exact requested URI now wins over a resource registered under its parsed form and over a matching template. With both
https://example.comandhttps://example.com/registered, a read ofhttps://example.comreturns the first instead of the second; if the first is disabled, the read is rejected as disabled instead of serving the second. Reads that match no exact key behave as before.Types of changes
Checklist
Additional context
v1.xhas the same lookup insrc/server/mcp.ts; this PR targetsmainonly.