Skip to content

Commit e7de46f

Browse files
alistair3149claude
andauthored
Stamp a real freshness hint on cacheable results (#523)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 73a6d95 commit e7de46f

3 files changed

Lines changed: 47 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
2020

2121
### Changed
2222

23+
- Cacheable results (tool and resource lists, wiki resource reads, discovery) now carry a 60-second freshness hint instead of `ttlMs: 0`, so clients on the 2026-07-28 revision can cache them between polls. Change notifications are unaffected.
2324
- The hosted OAuth sign-in's approval page is shorter and now names the address you will be returned to, including for a local application, where it previously said only "an application on this device". It no longer promises a permissions step the wiki does not always show.
2425
- The HTTP transport's own `401`, `503` and `413` replies carry new JSON-RPC error codes. Clients read the HTTP status for these conditions, so no change is expected; anything matching on the old codes `-32001` and `-32000` needs updating.
2526

src/server.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export interface CreateServerOptions {
3939
publisher?: ChangePublisher;
4040
}
4141

42+
// Everything cacheable here changes only when a reconcile pass runs (a wiki is
43+
// added or removed, or an extension probe flips), and subscribed clients learn
44+
// of that through listChanged events — the TTL only bounds staleness for
45+
// clients that poll instead. Private scope: tool and resource lists reflect
46+
// this deployment's wiki configuration, so a shared cache must not serve one
47+
// deployment's lists to another behind the same intermediary.
48+
const CACHE_HINT = { ttlMs: 60_000, cacheScope: 'private' } as const;
49+
4250
export const createServer = async (
4351
ctx: ToolContext,
4452
reqCtx?: Pick<McpRequestContext, 'era'>,
@@ -62,6 +70,13 @@ export const createServer = async (
6270
logging: {},
6371
},
6472
instructions: SERVER_INSTRUCTIONS,
73+
cacheHints: {
74+
'tools/list': CACHE_HINT,
75+
'resources/list': CACHE_HINT,
76+
'resources/templates/list': CACHE_HINT,
77+
'resources/read': CACHE_HINT,
78+
'server/discover': CACHE_HINT,
79+
},
6580
},
6681
);
6782

tests/transport/modernEra.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,33 @@ describe('2026-07-28 era over Streamable HTTP (in-memory)', () => {
8585
expect(result.isError ?? false).toBe(false);
8686
});
8787

88+
it('stamps the configured cache hints on cacheable modern-era results', async () => {
89+
const { handler } = buildHandler();
90+
cleanups.push(() => handler.close());
91+
92+
const client = new Client(
93+
{ name: 'cache-hint-test', version: '0.0.0' },
94+
{ versionNegotiation: { mode: { pin: '2026-07-28' } } },
95+
);
96+
cleanups.push(() => client.close());
97+
await client.connect(clientTransport(handler));
98+
99+
const tools = await client.listTools();
100+
expect(tools.ttlMs).toBe(60_000);
101+
expect(tools.cacheScope).toBe('private');
102+
103+
const resources = await client.listResources();
104+
expect(resources.ttlMs).toBe(60_000);
105+
expect(resources.cacheScope).toBe('private');
106+
107+
// resources/read is the one operation with a competing precedence path
108+
// (a per-registration cacheHint would override field by field), so pin
109+
// that the per-operation hint reaches an actual read result.
110+
const read = await client.readResource({ uri: 'mcp://wikis/test-wiki' });
111+
expect(read.ttlMs).toBe(60_000);
112+
expect(read.cacheScope).toBe('private');
113+
});
114+
88115
it('falls back to a legacy handshake through the same handler when negotiation is off', async () => {
89116
const { handler } = buildHandler();
90117
cleanups.push(() => handler.close());
@@ -95,6 +122,10 @@ describe('2026-07-28 era over Streamable HTTP (in-memory)', () => {
95122

96123
const tools = await client.listTools();
97124
expect(tools.tools.length).toBeGreaterThan(0);
125+
// Cache hints are a 2026-07-28 wire feature; a legacy-era response must
126+
// not carry them.
127+
expect(tools.ttlMs).toBeUndefined();
128+
expect(tools.cacheScope).toBeUndefined();
98129
});
99130

100131
it('delivers toolsChanged to a modern listen subscriber when a management tool reconciles', async () => {

0 commit comments

Comments
 (0)