-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core,client,server): ttl on list results (SEP-2549) #2070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -613,7 +613,13 @@ | |
| * An opaque token representing the pagination position after the last returned result. | ||
| * If present, there may be more results available. | ||
| */ | ||
| nextCursor: CursorSchema.optional() | ||
| nextCursor: CursorSchema.optional(), | ||
| /** | ||
| * How long (in seconds) this result may be considered fresh before re-fetching (SEP-2549). | ||
| * Allows clients to cache list responses and poll on a predictable schedule, supplementing | ||
| * (not replacing) the `list_changed` notification mechanism. | ||
| */ | ||
| ttl: z.number().int().nonnegative().optional() | ||
|
Check warning on line 622 in packages/core/src/types/schemas.ts
|
||
|
Comment on lines
+616
to
+622
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 This PR adds public API to three published packages — Extended reasoning...What's missingThis PR adds new public API surface to three separately-published packages but contains no changeset file:
The changeset-bot comment on the PR explicitly reports " Why the repo expects one
Step-by-step proof
Why nothing else covers itThis is distinct from the four inline comments already on the PR (spec.types.ts hand-edit — now resolved; ImpactProcess / release-hygiene only — there is no runtime defect, hence nit. The practical consequence is that SEP-2549 ships without a semver bump or CHANGELOG line, which (a) makes the feature undiscoverable in release notes and (b) means downstream lockfiles won't pick it up as a new version. Easy to fix and routinely caught at merge time, but worth flagging since the PR claims "non-breaking change which adds functionality", which is precisely the changeset Suggested fixAdd a single changeset (the bot's "Click here if you're a maintainer who wants to add a changeset" link pre-fills it), e.g.: ---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/server": minor
"@modelcontextprotocol/client": minor
---
feat: SEP-2549 list-result `ttl` — `PaginatedResult.ttl`, `ServerOptions.listTtlSeconds`, `Client.isToolCacheStale`( |
||
| }); | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,14 @@ export type ServerOptions = ProtocolOptions & { | |
| * @default {@linkcode DefaultJsonSchemaValidator} ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, `CfWorkerJsonSchemaValidator` on Cloudflare Workers) | ||
| */ | ||
| jsonSchemaValidator?: jsonSchemaValidator; | ||
|
|
||
| /** | ||
| * Optional `ttl` (in seconds) included on `tools/list`, `prompts/list`, `resources/list`, | ||
| * and `resources/templates/list` responses (SEP-2549). Tells clients how long the response | ||
| * may be considered fresh; clients may cache and re-poll on this schedule. Supplements, does | ||
| * not replace, the `list_changed` notification mechanism. | ||
| */ | ||
| listTtlSeconds?: number; | ||
|
Comment on lines
+90
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Extended reasoning...What the issue isThe new This makes How it manifestsA user of the low-level API writes: const server = new Server({ name: 's', version: '1' }, { listTtlSeconds: 60 });
server.setRequestHandler('tools/list', () => ({ tools: [...] }));IntelliSense surfaces Why existing code doesn't prevent itThere is no separate Step-by-step proof
Contrast with ImpactLow — Suggested fixEither:
Comment on lines
+90
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 This PR adds two new public API knobs ( Extended reasoning...What's missingREVIEW.md's Tests & docs checklist says: "New feature: verify prose documentation is added (not just JSDoc), and assess whether examples/ needs a new or updated example." This PR introduces two user-facing API surfaces:
Both have JSDoc, but neither appears in the prose docs. Step-by-step proof
Why existing artifacts don't cover itJSDoc surfaces in IntelliSense and the generated API reference, but the repo's review checklist explicitly distinguishes prose documentation from JSDoc, and the ImpactDocumentation-completeness only — no runtime correctness impact, hence nit. The convention is also not perfectly uniform (not every Suggested fix
|
||
| }; | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The new public
Client.isToolCacheStalegetter (and the_maybeExpireToolCache/ ≥1s-clamp / reset-on-listTools()logic behind it) has no test coverage — grepping forisToolCacheStale/toolCacheExpiresAtmatches onlyclient.ts. The PR description claims "New tests for ttl population + client staleness flag", but the added tests inmcp.test.tsonly assert server-sidettlpopulation; a fake-timer test exercising expiry, the clamp, and the stale-flag reset would close the gap.Extended reasoning...
What's missing
Clientgains a new public API surface in this PR:get isToolCacheStale(): boolean— calls_maybeExpireToolCache()then returns_toolCacheStale._maybeExpireToolCache()— flips_toolCacheStale = trueand clears_toolCacheExpiresAtonceDate.now() >= _toolCacheExpiresAt.cacheToolMetadata(tools, ttlSeconds?)— now sets_toolCacheExpiresAt = Date.now() + Math.max(1, ttlSeconds) * 1000(the ≥1s clamp) and resets_toolCacheStale = falseon every freshlistTools().Grepping the repo for
isToolCacheStale|toolCacheStale|toolCacheExpiresAtreturns onlypackages/client/src/client/client.ts. No test file — neitherpackages/client/test/**nortest/integration/**— references the getter or the underlying state.Why the PR description is misleading
"How Has This Been Tested?" says: "New tests for ttl population + client staleness flag". The diff adds exactly two tests to
test/integration/test/server/mcp.test.ts:should include ttl on list results when listTtlSeconds is configured— assertsresult.ttl === 60on the four list endpoints, callingclient.request({ method: '…/list' })directly (which bypassesClient.listTools()and therefore never touchescacheToolMetadata).should omit ttl on list results when listTtlSeconds is not configured— asserts the property is absent.Both are server-side wire-format tests. Neither calls
client.listTools(), neither readsclient.isToolCacheStale, and neither advances time. The "client staleness flag" half of the claim is not backed by the diff — this is the REVIEW.md recurring catch "prose that promises behavior the code no longer ships."Step-by-step proof
rg -l 'isToolCacheStale'→packages/client/src/client/client.ts(1 hit, the definition).rg -l 'toolCacheExpiresAt|_toolCacheStale'→ same single file.mcp.test.tscases: they useclient.request(…)(raw protocol), notclient.listTools(), so evencacheToolMetadata(tools, result.ttl)is never reached in those tests.isToolCacheStaleflipping totrueafter the ttl elapses, (b)Math.max(1, ttlSeconds)clamping a server-suppliedttl: 0, (c)_toolCacheStaleresetting tofalseon a freshlistTools(), is exercised anywhere.Why it matters
REVIEW.md checklist: "New behavior has vitest coverage including error paths".
isToolCacheStaleis a new public getter on theClientclass with non-trivial, time-dependent semantics (and a security-adjacent clamp whose comment says "so a server cannot force immediate expiry of the validator cache"). It also currently has zero callsites in the SDK, so tests are the only thing that would catch a regression. Shipping it untested while the PR description asserts otherwise is worth flagging.Suggested fix
Add a vitest case (e.g. in
packages/client/test/client/client.test.tsor alongside the new integration tests) usingvi.useFakeTimers():This covers the getter, the clamp, and the reset in one place and makes the PR description accurate.