Skip to content

fix(server): read a static resource by the URI it is listed under - #3074

Open
mkantautas wants to merge 1 commit into
modelcontextprotocol:mainfrom
mkantautas:fix/resource-read-listed-uri
Open

mkantautas wants to merge 1 commit into
modelcontextprotocol:mainfrom
mkantautas:fix/resource-read-listed-uri

Conversation

@mkantautas

Copy link
Copy Markdown

McpServer lists a static resource under the string passed to registerResource(), but resources/read looked it up by new URL(uri).toString(). A URI that the URL parser serializes differently is listed and then not found:

server.registerResource('home', 'https://example.com', {}, async uri => ({ contents: [{ uri: uri.href, text: 'home' }] }));

const { resources } = await client.listResources(); // [{ uri: 'https://example.com', name: 'home' }]
await client.readResource({ uri: resources[0].uri }); // rejects: Resource not found: https://example.com (-32602)

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/list should get that resource. The Python SDK looks up concrete resources by the exact string the request carries.

What changed

  • resources/read, and the resources/read branch 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.
  • Changeset: patch for @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 when https://example.com and https://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 as https://example.com answers 403 before the read callback runs (200 on main), and a disabled https://example.com resolves no challenge from an enabled https://example.com/ twin (main resolves the twin's challenge).
  • In Linux containers: pnpm run check:all, build:all and scripts/smoke-dist-types.mjs pass; pnpm -r --filter '!@modelcontextprotocol/test-e2e' test passes 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-examples and docs:examples pass on Node 24 (one run-examples run had an oauth-client-credentials leg fail with ECONNREFUSED at startup; it passed on the rerun and on main). test-e2e passes on Node 20. On Node 22 and 24 the timeout scenarios in test/e2e/scenarios/protocol.test.ts fail or leave unhandled REQUEST_TIMEOUT rejections in my containers, the same way on main (related: [v2] e2e protocol:timeout:max-total is 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.com and https://example.com/ registered, a read of https://example.com returns 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

v1.x has the same lookup in src/server/mcp.ts; this PR targets main only.

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.
@mkantautas
mkantautas requested a review from a team as a code owner October 23, 2026 01:48
@changeset-bot

changeset-bot Bot commented Oct 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a5db863

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/core-internal Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Oct 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@3074

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@3074

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@3074

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@3074

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@3074

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@3074

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@3074

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@3074

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@3074

commit: a5db863

@arhancanli

Copy link
Copy Markdown

Read the diff; I did not run the suite. One case the new tests do not cover: the lookup is now registered[requestedString] ?? registered[new URL(requested).toString()], so it works when the client sends the string exactly as listed, but a client that normalizes before reading (for example it keeps URL objects and sends url.href) still misses a resource registered as https://Example.com/docs: the requested https://example.com/docs matches neither the exact key nor the parsed key (the parsed form of the request is the lowercase one, the registered key is not). That was already the case before this PR, so it may be out of scope, but a test that pins the intended behavior (either "exact string only" or "also match registered keys by their parsed form") would make the contract explicit. If it is the first, a line in the registerResource docs saying listed URIs must be read back verbatim would help.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 23, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants