Skip to content

fix(server): reflect resource title updates in list responses - #3005

Merged
felixweinberger merged 1 commit into
modelcontextprotocol:mainfrom
chaeyn:fix/resource-title-update/#3004
Oct 12, 2026
Merged

felixweinberger merged 1 commit into
modelcontextprotocol:mainfrom
chaeyn:fix/resource-title-update/#3004

Conversation

@chaeyn

@chaeyn chaeyn commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Motivation and Context

update({ title: 'New title' }) changes a registered resource or resource template's handle, but list responses still use the original metadata.title. Clients therefore receive the old title even when bypassing their response cache. If registration supplied no title, the new title is missing from the response.

Fixes #3004.

What changed

  • packages/server/src/server/mcp.ts: synchronize the metadata title in both resource update paths, including when other metadata is replaced in the same call.
  • test/integration/test/server/resourceTitleUpdate.test.ts: add seven client/server regression cases for initial title presence/absence, simultaneous metadata updates, and template title inheritance. Individual resource titles retain precedence over template metadata.
  • Changeset (patch: server).

How Has This Been Tested?

  • All seven new regression tests fail on the base source and pass with the fix; focused MCP integration tests: 129 passed.
  • pnpm build:all, pnpm typecheck:all, and pnpm lint:all pass, including pre-push hooks.
  • All 14 GitHub checks pass on 142c861, including Node 20/22/24 E2E, Bun/Deno, and client/server conformance.
  • Locally, pnpm test:all passes the non-E2E packages but exits nonzero on two unhandled REQUEST_TIMEOUT rejections in protocol.test.ts. Both reproduce with the unmodified mcp.ts from b022522. A standalone protocol-file rerun also had a max-total timing assertion failure; isolated max-total cases pass on both base and patched source.

Local environment: Node v24.19.0, pnpm 10.26.1, macOS. Reproduced with SDK client/server instances over InMemoryTransport; no production-application validation.

Breaking Changes

None. No API or configuration changes are required.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • Error handling reviewed: no additional error handling is needed for this metadata synchronization fix.
  • I have added or updated documentation as needed (patch changeset)

AI assistance: Codex was used for the investigation, implementation, tests, and PR description.

@chaeyn
chaeyn requested a review from a team as a code owner October 12, 2026 02:32
@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 142c861

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

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 12, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 142c861

@chaeyn
chaeyn force-pushed the fix/resource-title-update/#3004 branch from 0c352e0 to 142c861 Compare October 12, 2026 02:34
@chaeyn chaeyn changed the title fix: resource title update/#3004 fix(server): reflect resource title updates in list responses Oct 12, 2026
@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 12, 2026
@felixweinberger

Copy link
Copy Markdown
Contributor

@claude review

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked whether prompts and tools have the same title/metadata divergence — their list handlers (mcp.ts:272, :644) read title straight from the registered object, so they are not affected. I also checked the update({ title: null }) schema-invalid case — the update types declare title?: string, so null is rejected at the type level.

Extended reasoning...

The change moves title handling after metadata assignment in the resource and resource-template update callbacks in packages/server/src/server/mcp.ts and mirrors the title into metadata so resources/list and resources/templates/list reflect it; it touches no security-sensitive surface. Two inline findings (title lost on a later metadata-only update, and the pre-existing stale-URI key on repeated uri updates) were posted, so a human should weigh those before merging.

Comment on lines 783 to +787
if (updates.metadata !== undefined) registeredResource.metadata = updates.metadata;
if (updates.title !== undefined) {
registeredResource.title = updates.title;
registeredResource.metadata = { ...registeredResource.metadata, title: updates.title };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 pre-existing, not blocking: Server authors who update a title and later replace metadata will see the title vanish from resources/list while the handle still reports the new title. The sync at packages/server/src/server/mcp.ts:783-787 only runs inside the call that carries title; a following update({ metadata }) at mcp.ts:783 overwrites metadata wholesale and drops the synced title key, and mcp.ts:542 spreads only metadata. The PR makes title the authoritative field but only for one call. Fix: treat registeredResource.title as the source of truth in list handlers (or re-apply it whenever metadata is replaced), in both the resource and template update paths (mcp.ts:824-828).
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

Sequence: resource.update({ title: 'New' }) then resource.update({ metadata: { description: 'd' } }). After the first call mcp.ts:786 stores metadata.title = 'New' and the wire shows it. The second call at mcp.ts:783 assigns metadata = updates.metadata, discarding the title key; mcp.ts:784 does not run because updates.title is undefined. resources/list at mcp.ts:542 and the template path at mcp.ts:554/568 spread only metadata, so clients now get no title at all, while registeredResource.title still reads 'New'. The base branch also lost the title in this sequence, but the base never surfaced handle.title on the wire at all; this PR introduces the contract that update({ title }) is reflected in list responses and its changeset says other metadata is preserved, so users will now rely on the handle title persisting. Remedy: have the list handlers emit title from the handle or re-merge the stored title whenever updates.metadata replaces the object, for both resources and templates.

Verification: pre-existing — triggered when a server calls update({ title }) and then, in a separate call, update({ metadata }) without a title key. packages/server/src/server/mcp.ts:783 replaces the metadata object wholesale, and the title mirror at mcp.ts:784-787 only runs when updates.title !== undefined; resources/list at mcp.ts:542 spreads only resource.metadata. The base branch ends in the same state.

Comment on lines 780 to 781
if (updates.uri) this._registeredResources[updates.uri] = registeredResource;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 pre-existing, not blocking: Server authors who move a resource with update({ uri }) and then call remove() or a second update({ uri }) leave the resource registered under the previous URI, so resources/list keeps advertising it and resources/read still serves it. The update callback at mcp.ts:778-781 compares against and deletes the closure-captured original uri, never the current key. The same pattern exists for template names at mcp.ts:819-822. Fix: track the current uri (and template name) in a mutable variable updated on each rename, and delete that key, so remove() and chained renames act on the live registration.
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

Trigger: resource.update({ uri: 'b' }) then resource.remove() (which calls update({ uri: null }) at mcp.ts:776), or a second update({ uri: 'c' }). mcp.ts:778 tests updates.uri !== uri where uri is the registration-time parameter captured at mcp.ts:762, and mcp.ts:779 deletes this._registeredResources[uri], which no longer holds the resource. The entry under 'b' survives, so resources/list at mcp.ts:537 still lists it and resources/read at mcp.ts:586 still serves it; after a second rename the resource is registered twice under 'b' and 'c'. The template path at mcp.ts:819-822 has the identical flaw with the captured name. The base branch behaves the same, which is why it was dismissed, but this PR edits exactly this update callback in both places and adds a changeset promising update() is reflected in list responses, so the review had the function open and the defect sits in the lines directly above the hunk. Remedy: keep a let currentUri / currentName that each rename updates and delete that key.

Verification: pre-existing — the base at b022522 fails by the same route; this PR does not touch the rename branch. Trigger: a server author calls resource.update({ uri: 'b' }) and later resource.remove(). Mechanism at packages/server/src/server/mcp.ts:778-781: uri is the registration-time parameter never reassigned, so remove() is a no-op and the 'b' entry survives.

@felixweinberger
felixweinberger merged commit b381559 into modelcontextprotocol:main Oct 12, 2026
18 checks passed
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.

[v2] Resource update({ title }) leaves list response titles unchanged

2 participants