Skip to content

Commit cea8a93

Browse files
snapsynapseclaude
andcommitted
Make the template MCP server dual-era for spec revision 2026-07-28
This template is a generator: every project forked from it inherited a 2024-11-05 protocol pin, and scripts/eval.js asserted that handshake, so the stale shape was test-enforced across all downstream forks. - mcp-server.js: supportedVersions ["2026-07-28", "2024-11-05"]; add server/discover (required by the new spec), resultType on every result, ttlMs and cacheScope on tools/list, and -32022 with data.supported for unsupported _meta versions. The legacy initialize handshake is unchanged, and requests without _meta are served exactly as before, so bare and handshake-less callers keep working. - tests/fixtures/mcp-request.jsonl: legacy lines kept; added a modern server/discover request carrying full _meta. - scripts/eval.js: new "MCP dual-era contract" eval asserting the discover shape, ttlMs and cacheScope on tools/list, and -32022 on a bogus version. Existing legacy assertions still pass. - PUBLISH-MCP.md: smoke tests carry the modern _meta envelope, plus a short section on the stateless revision and dual-era posture. - README.md: state the dual-era support so forks know what they get. - MANIFEST.yaml: hashes refreshed via scripts/validate-hashes.sh. node scripts/eval.js: 33/33 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0956093 commit cea8a93

6 files changed

Lines changed: 98 additions & 12 deletions

File tree

‎MANIFEST.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ files:
4747

4848
- path: README.md
4949
role: protocol
50-
hash: sha256:c2e411e2234d7cdec0b89f42af402919f62633e7fd600399abf47e27307362e9
50+
hash: sha256:dfebb03cd3f65629151a36d66eb7ed84a1918b5208e2ea6f6b2c41a4870b2141
5151
note: Template overview
5252

5353
- path: favicon.svg
@@ -122,12 +122,12 @@ files:
122122

123123
- path: scripts/eval.js
124124
role: tooling
125-
hash: sha256:1f441afee32a8f0ad72986cba5acd227a67400c0bbff53655ce3c0a5fa8db4a7
125+
hash: sha256:c2d73494bb777a6a23a78893ac38162d99bf252620d748ca27e5a9b504062ea9
126126
note: Behavioral and security regression suite
127127

128128
- path: mcp-server.js
129129
role: tooling
130-
hash: sha256:7a6e590449baa4eaff376e8a23d5bedbcf8a546d1a75d149287bc0c13517840f
130+
hash: sha256:47dbdf6fd84013df6407ed538c72816d758c4531b65be11b1d95861e1d7467da
131131
note: Read-only MCP interface over canonical project data
132132

133133
- path: scripts/check-links.js

‎PUBLISH-MCP.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ This guide captures the exact prep work, in the order it has to happen.
1414
using only `a-z`, `0-9`, and single hyphens. Run `node scripts/validate.js`
1515
before publishing so unsafe IDs are rejected before they become MCP-visible
1616
data.
17-
- `node mcp-server.js` works locally — pipe `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}`
17+
- `node mcp-server.js` works locally — pipe
18+
`{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"smoke-test","version":"0.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}`
1819
into it and you should get back your tools.
1920
- An npm account ([npmjs.com](https://npmjs.com/signup)) with 2FA enabled.
2021
- A GitHub account that matches the org/user owning the repo.
@@ -99,11 +100,20 @@ add it to `files` and re-run. If anything is included that shouldn't be (a
99100
Then test that the MCP server still runs after the package.json changes:
100101

101102
```bash
102-
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node mcp-server.js
103+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"smoke-test","version":"0.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' | node mcp-server.js
103104
```
104105

105106
You should get back your tools.
106107

108+
That smoke test is the *stateless* form from the 2026-07-28 spec revision: no
109+
`initialize` handshake, the protocol version rides in each request's `_meta`,
110+
and `server/discover` is the new MUST-implement discovery method. The server
111+
in this template is dual-era — a legacy client that opens with `initialize`
112+
still gets the 2024-11-05 handshake it expects, for as long as the deprecation
113+
window runs, while modern clients are served statelessly in the same process.
114+
A `_meta` protocol version the server doesn't support gets error `-32022`
115+
back, not a guess; a request with no `_meta` at all is served as legacy.
116+
107117
The server follows JSON-RPC notification semantics. Messages without an `id`
108118
are notifications and do not receive responses, including unknown-method
109119
notifications.
@@ -129,7 +139,7 @@ Verify:
129139

130140
```bash
131141
npm view your-package-name version
132-
npx -y your-package-name <<< '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
142+
npx -y your-package-name <<< '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"smoke-test","version":"0.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
133143
```
134144

135145
A cold install + tools list confirms the package is real.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ This canonical repository tracks only the generated `demo/` published at `https:
6363
- **Dark/light theme** — with persistence
6464
- **Client-side search** — lazy-loaded, keyboard-navigable
6565
- **Sortable tables** — click any column header to sort
66-
- **Local MCP server** — optional read-only agent access through `mcp-server.js`; it runs separately from the static build
66+
- **Local MCP server** — optional read-only agent access through `mcp-server.js`; it runs separately from the static build. Dual-era: speaks MCP spec 2026-07-28 (stateless, `server/discover`, cacheable tool lists) and legacy 2024-11-05, so generated projects work with both current and older MCP clients
6767
- **Discovery files** — llms.txt, agents.json, RSS for machine consumption
6868
- **Safe generated output** — generated links, CSS tokens, and client-side comparison labels are normalized or escaped
6969
- **Subpath-safe deployment** — canonical, discovery, 404, and asset URLs preserve a configured path such as `/demo/`

‎mcp-server.js‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,22 +307,46 @@ function handleToolCall(name, args) {
307307
// JSON-RPC / MCP protocol
308308
// ---------------------------------------------------------------------------
309309

310+
// Dual-era server (MCP spec revision 2026-07-28): a legacy client selects the
311+
// 2024-11-05 semantics via `initialize`; a modern client carries per-request
312+
// `_meta` and is served statelessly. Both eras run concurrently in-process.
313+
const SUPPORTED_PROTOCOL_VERSIONS = ['2026-07-28', '2024-11-05'];
314+
// Static data regenerated on a known cadence — safe to cache for an hour.
315+
const CACHE_TTL_MS = 3600000;
316+
const CACHE_SCOPE = 'public';
317+
310318
function makeResponse(id, result) {
311319
return JSON.stringify({ jsonrpc: '2.0', id, result });
312320
}
313321

314-
function makeError(id, code, message) {
315-
return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
322+
function makeError(id, code, message, data) {
323+
const error = { code, message };
324+
if (data !== undefined) error.data = data;
325+
return JSON.stringify({ jsonrpc: '2.0', id, error });
316326
}
317327

318328
function handleMessage(msg) {
319329
const { id, method, params } = msg;
320330
const hasId = Object.prototype.hasOwnProperty.call(msg, 'id');
321331

332+
// Modern requests declare their protocol version per request. Unknown
333+
// versions are rejected; requests without `_meta` (legacy handshake or
334+
// bare requests) are served exactly as before.
335+
const requestedVersion = params?._meta?.['io.modelcontextprotocol/protocolVersion'];
336+
if (requestedVersion !== undefined && !SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)) {
337+
if (!hasId) return null;
338+
return makeError(id, -32022, 'Unsupported protocol version', {
339+
supported: SUPPORTED_PROTOCOL_VERSIONS,
340+
requested: requestedVersion
341+
});
342+
}
343+
322344
if (method === 'initialize') {
323345
if (!hasId) return null;
346+
const clientVersion = params?.protocolVersion;
324347
return makeResponse(id, {
325-
protocolVersion: '2024-11-05',
348+
resultType: 'complete',
349+
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion) ? clientVersion : '2024-11-05',
326350
capabilities: { tools: {} },
327351
serverInfo: {
328352
name: config.name || 'Knowledge Base MCP',
@@ -335,17 +359,41 @@ function handleMessage(msg) {
335359
return null; // notification, no response
336360
}
337361

362+
if (method === 'server/discover') {
363+
if (!hasId) return null;
364+
const result = {
365+
resultType: 'complete',
366+
supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
367+
capabilities: { tools: {} },
368+
_meta: {
369+
'io.modelcontextprotocol/serverInfo': {
370+
name: config.name || 'Knowledge Base MCP',
371+
version: packageMetadata.version
372+
}
373+
},
374+
ttlMs: CACHE_TTL_MS,
375+
cacheScope: CACHE_SCOPE
376+
};
377+
if (config.description) result.instructions = config.description;
378+
return makeResponse(id, result);
379+
}
380+
338381
if (method === 'tools/list') {
339382
if (!hasId) return null;
340-
return makeResponse(id, { tools: getToolDefinitions() });
383+
return makeResponse(id, {
384+
resultType: 'complete',
385+
tools: getToolDefinitions(),
386+
ttlMs: CACHE_TTL_MS,
387+
cacheScope: CACHE_SCOPE
388+
});
341389
}
342390

343391
if (method === 'tools/call') {
344392
if (!hasId) return null;
345393
const toolName = params?.name;
346394
const toolArgs = params?.arguments || {};
347395
const result = handleToolCall(toolName, toolArgs);
348-
return makeResponse(id, result);
396+
return makeResponse(id, { resultType: 'complete', ...result });
349397
}
350398

351399
if (!hasId) return null;

‎scripts/eval.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,7 @@ function evalMcpSmoke() {
854854
assertIncludes(interactive.stdout, 'iso-27001');
855855
assertIncludes(interactive.stdout, 'talking_point');
856856
assertIncludes(interactive.stdout, 'sources');
857+
assertIncludes(interactive.stdout, 'supportedVersions');
857858
}
858859

859860
function evalMcpResponseShape() {
@@ -879,6 +880,31 @@ function evalMcpResponseShape() {
879880
assertIncludes(missing.error, 'not found: missing-id');
880881
}
881882

883+
function evalMcpDualEraContract() {
884+
const meta = {
885+
'io.modelcontextprotocol/protocolVersion': '2026-07-28',
886+
'io.modelcontextprotocol/clientInfo': { name: 'eval', version: '1.0.0' },
887+
'io.modelcontextprotocol/clientCapabilities': {}
888+
};
889+
const input = [
890+
{ jsonrpc: '2.0', id: 1, method: 'server/discover', params: { _meta: meta } },
891+
{ jsonrpc: '2.0', id: 2, method: 'tools/list', params: { _meta: meta } },
892+
{ jsonrpc: '2.0', id: 3, method: 'tools/list', params: { _meta: { 'io.modelcontextprotocol/protocolVersion': '1900-01-01' } } }
893+
].map(message => JSON.stringify(message)).join('\n') + '\n';
894+
const result = spawnSync(process.execPath, ['mcp-server.js'], { cwd: ROOT, encoding: 'utf8', input });
895+
assert.strictEqual(result.status, 0, `MCP dual-era check failed:\n${result.stdout}\n${result.stderr}`);
896+
const responses = result.stdout.trim().split('\n').map(line => JSON.parse(line));
897+
const discover = responses[0].result;
898+
assert.strictEqual(discover.resultType, 'complete');
899+
assert.ok(discover.supportedVersions.includes('2026-07-28'), 'server/discover must advertise 2026-07-28.');
900+
assert.ok(discover.capabilities.tools, 'server/discover must advertise the tools capability.');
901+
const list = responses[1].result;
902+
assert.strictEqual(typeof list.ttlMs, 'number', 'tools/list must carry a numeric ttlMs.');
903+
assert.strictEqual(list.cacheScope, 'public');
904+
assert.strictEqual(responses[2].error.code, -32022);
905+
assert.ok(Array.isArray(responses[2].error.data.supported), 'UnsupportedProtocolVersionError must list supported versions.');
906+
}
907+
882908
function evalGeneratedArtifactCleanliness() {
883909
const forbiddenNames = new Set(['.DS_Store', 'Thumbs.db', 'CNAME']);
884910
const tracked = runCommand('git', ['ls-files', '-z', 'demo']);
@@ -1005,6 +1031,7 @@ const evals = [
10051031
['HTML snapshots', evalHtmlSnapshots],
10061032
['MCP smoke', evalMcpSmoke],
10071033
['MCP response shape', evalMcpResponseShape],
1034+
['MCP dual-era contract', evalMcpDualEraContract],
10081035
['MCP notification silence', evalMcpNotificationSilence],
10091036
['generated artifact cleanliness', evalGeneratedArtifactCleanliness],
10101037
['workflow contract', evalWorkflowContract],

‎tests/fixtures/mcp-request.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"eval","version":"1.0.0"}}}
22
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
33
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_framework","arguments":{"id":"iso-27001"}}}
4+
{"jsonrpc":"2.0","id":4,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"eval","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}

0 commit comments

Comments
 (0)