Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Claude Code could not connect to the Torque MCP server (refused since 2026-10-04).**
Claude Code (2.1.308 desktop-bundled and 2.1.310 on PATH, recorded 2026-10-27 through a
pass-through stdio shim) probes with `server/discover` (id `server-discover-probe-1`),
reads the version list, then sends `initialize` with `2025-11-25` on the same
connection. The kernel pinned the connection modern on the probe and refused that
`initialize` with -32010 ("eras never mix"), so every connect failed. `server/discover`
now answers (still modern-shaped, still version-exempt) without pinning an era; the
first era-bearing message pins it — `initialize` pins legacy, a request carrying the
`_meta` protocol version pins modern. Everything else about "eras never mix" holds:
discover after `initialize`, a modern-marked request on a legacy connection, and
`initialize` after a real modern request all stay refused, and ambiguous or incomplete
handshakes still pin nothing. The unpinned-connection refusal no longer names discover
as a way to pin. Falsifier: the recorded two-line handshake, replayed over stdio,
seen red (-32010) against the unpatched kernel.
- **One store per project, and a bounded touch trail (found 2026-10-27 reading a live
install; all CLI-enforced, each test seen red against the unpatched tree):**
- **One base for every caller.** `baseDir()` honored `$CLAUDE_PLUGIN_DATA`, which Claude
Expand Down
33 changes: 20 additions & 13 deletions src/mcp/rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
// resources, no state — those arrive in later steps through the `methods` table.
//
// Era pinning is the one invariant this module exists to enforce: a connection
// speaks exactly one protocol era. `server/discover` (protocol 2026-07-28,
// stateless, _meta-carried version) pins modern; `initialize` (a named
// request/response revision) pins legacy; after the pin, the other era's
// surface is a named refusal — never a silent downgrade, never a mixed
// connection. Mixing is how a stateless request inherits handshake state it
// never negotiated.
// speaks exactly one protocol era. The first era-bearing message pins it: a
// request carrying the 2026-07-28 _meta protocol version pins modern;
// `initialize` (a named request/response revision) pins legacy; after the pin,
// the other era's surface is a named refusal — never a silent downgrade, never
// a mixed connection. Mixing is how a stateless request inherits handshake
// state it never negotiated. `server/discover` pins nothing: it negotiates no
// state, and clients probe with it before choosing an era on the same
// connection (Claude Code does exactly that, then sends a legacy initialize).

const JSONRPC = '2.0';
const META = 'io.modelcontextprotocol/';
Expand Down Expand Up @@ -86,7 +88,7 @@ function createKernel({ serverInfo, capabilities, methods }) {
return rpcError(
ERR_ERA_PINNED,
`this connection has no era yet and ${method} needs one — ` +
`knock with server/discover (modern) or initialize (legacy) first`
`send initialize (legacy) or a request carrying _meta ${META}protocolVersion (modern) first`
);
}
return rpcError(
Expand All @@ -96,7 +98,8 @@ function createKernel({ serverInfo, capabilities, methods }) {
);
}

// Built-ins pin the era. Everything else routes through the method table.
// initialize pins legacy; discover answers and pins nothing. Everything
// else routes through the method table.
function dispatch(msg) {
const method = msg.method;

Expand Down Expand Up @@ -136,9 +139,10 @@ function createKernel({ serverInfo, capabilities, methods }) {
if (method === 'server/discover') {
// Version-exempt by design (attack-round-1 ruling): discover is the
// door a client knocks to learn what versions exist — demanding proof
// of a version before answering that question is circular.
// of a version before answering that question is circular. Answering
// it chooses no era either: the client reads the list and THEN picks,
// on this same connection.
if (era === 'legacy') throw eraRefusal(method);
era = 'modern';
return {
protocolVersions: [MODERN_VERSION, ...LEGACY_VERSIONS],
capabilities: caps,
Expand Down Expand Up @@ -204,8 +208,9 @@ function createKernel({ serverInfo, capabilities, methods }) {

const isNotification = id === undefined;
if (isNotification && (msg.method === 'initialize' || msg.method === 'server/discover')) {
// A handshake needs an answer to exist; a notification-shaped one can
// only pin an era silently, which poisons the connection. Dropped.
// A handshake needs an answer to exist; a notification-shaped initialize
// can only pin an era silently, which poisons the connection, and a
// discover with no id asks a question nobody hears answered. Dropped.
dropped++;
return null;
}
Expand Down Expand Up @@ -237,7 +242,9 @@ function createKernel({ serverInfo, capabilities, methods }) {
if (typeof result !== 'object' || result === null || Array.isArray(result)) {
throw rpcError(ERR_INTERNAL, `handler for ${msg.method} returned a result that does not serialize to an object`);
}
if (era === 'modern') {
// discover is a 2026-07-28 method, so its answer is modern-shaped
// even on the unpinned connection it leaves behind.
if (era === 'modern' || msg.method === 'server/discover') {
// Modern results always name themselves: resultType is required by
// the protocol, and serverInfo travels in _meta because there is no
// handshake left to have said it once. Decoration works on a spread
Expand Down
55 changes: 47 additions & 8 deletions test/mcp-rpc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,16 @@ ok('a batch array is refused by name — MCP removed batching', () => {

// --- modern era: server/discover -------------------------------------------

ok('server/discover advertises versions, capabilities, identity — and pins modern', () => {
ok('server/discover advertises versions, capabilities, identity — and pins nothing', () => {
const conn = kernel().createConnection();
const res = conn.handleMessage({ jsonrpc: '2.0', id: 1, method: 'server/discover' });
assert.strictEqual(res.error, undefined, res.error && res.error.message);
assert.deepStrictEqual(res.result.protocolVersions, [MODERN, LEGACY, CODEX_LEGACY]);
assert.deepStrictEqual(res.result.serverInfo, { name: 'torque-mcp-test', version: '0.0.0' });
assert.ok(res.result.capabilities, 'capabilities must be advertised');
assert.strictEqual(res.result.resultType, 'complete');
assert.strictEqual(conn.era(), 'modern');
assert.strictEqual(res.result.resultType, 'complete', 'discover is a modern method and answers modern-shaped');
assert.deepStrictEqual(res.result._meta[META + 'serverInfo'], { name: 'torque-mcp-test', version: '0.0.0' });
assert.strictEqual(conn.era(), null, 'asking which versions exist chooses none of them');
});

ok('a modern result carries resultType and serverInfo in _meta on every response', () => {
Expand Down Expand Up @@ -217,15 +218,53 @@ ok('notifications/initialized is accepted silently on a legacy connection', () =

// --- era pinning: never mixed -----------------------------------------------

ok('initialize after server/discover is refused — eras never mix', () => {
ok('initialize after server/discover opens a legacy session — the probe chose no era', () => {
const conn = kernel().createConnection();
conn.handleMessage({ jsonrpc: '2.0', id: 1, method: 'server/discover' });
const res = conn.handleMessage({ jsonrpc: '2.0', id: 2, method: 'initialize', params: legacyInit() });
assert.strictEqual(res.error, undefined, res.error && res.error.message);
assert.strictEqual(res.result.protocolVersion, LEGACY);
assert.strictEqual(conn.era(), 'legacy');
});

ok('initialize after a modern request is refused — eras never mix', () => {
const conn = kernel({
'echo/params': { eras: ['modern'], handler: () => ({}) },
}).createConnection();
conn.handleMessage({ jsonrpc: '2.0', id: 1, method: 'echo/params', params: { _meta: modernMeta() } });
const res = conn.handleMessage({ jsonrpc: '2.0', id: 2, method: 'initialize', params: legacyInit() });
assert.strictEqual(res.error.code, rpc.ERR_ERA_PINNED);
assert.match(res.error.message, /modern/);
assert.match(res.error.message, /reconnect/i, 'the refusal must hand the client its remedy');
});

// Recorded 2026-10-27 from Claude Code 2.1.308 and 2.1.310 through a pass-through
// stdio shim: the client probes with server/discover (carrying the modern _meta),
// reads the version list, and opens a legacy session on the SAME connection.
// Pinning modern on the probe refused every Claude Code connect from 2026-10-04.
ok('the recorded Claude Code handshake — discover probe, then initialize 2025-11-25 — succeeds', () => {
const input = new PassThrough();
const output = new PassThrough();
const lines = [];
output.on('data', (chunk) => lines.push(...String(chunk).split('\n').filter((l) => l.length)));
const conn = kernel().createConnection();
stdio.attach({ createConnection: () => conn }, { input, output });
const clientInfo = '{"name":"claude-code","title":"Claude Code","version":"2.1.310","description":"Anthropic\'s agentic coding tool","websiteUrl":"https://claude.com/claude-code"}';
const clientCaps = '{"roots":{"listChanged":true},"elicitation":{"form":{},"url":{}},"extensions":{"io.modelcontextprotocol/tasks":{}}}';
input.write('{"jsonrpc":"2.0","id":"server-discover-probe-1","method":"server/discover","params":{"_meta":{' +
`"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":${clientInfo},` +
`"io.modelcontextprotocol/clientCapabilities":${clientCaps}}}}\n`);
input.write(`{"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":${clientCaps},` +
`"clientInfo":${clientInfo}},"jsonrpc":"2.0","id":0}\n`);
const [discover, init] = lines.map((l) => JSON.parse(l));
assert.strictEqual(discover.id, 'server-discover-probe-1');
assert.deepStrictEqual(discover.result.protocolVersions, [MODERN, LEGACY, CODEX_LEGACY]);
assert.strictEqual(init.id, 0);
assert.strictEqual(init.error, undefined, init.error && init.error.message);
assert.strictEqual(init.result.protocolVersion, LEGACY);
assert.strictEqual(conn.era(), 'legacy');
});

ok('server/discover after initialize is refused — eras never mix', () => {
const conn = kernel().createConnection();
conn.handleMessage({ jsonrpc: '2.0', id: 1, method: 'initialize', params: legacyInit() });
Expand Down Expand Up @@ -510,8 +549,8 @@ ok('C14 notifications/initialized moves no era, anywhere', () => {
legacy.handleMessage({ jsonrpc: '2.0', id: 1, method: 'initialize', params: legacyInit() });
assert.strictEqual(legacy.handleMessage({ jsonrpc: '2.0', method: 'notifications/initialized' }), null);
assert.strictEqual(legacy.era(), 'legacy');
const modern = kernel().createConnection();
modern.handleMessage({ jsonrpc: '2.0', id: 1, method: 'server/discover' });
const modern = kernel({ 'echo/params': { eras: ['modern'], handler: () => ({}) } }).createConnection();
modern.handleMessage({ jsonrpc: '2.0', id: 1, method: 'echo/params', params: { _meta: modernMeta() } });
assert.strictEqual(modern.handleMessage({ jsonrpc: '2.0', method: 'notifications/initialized' }), null);
assert.strictEqual(modern.era(), 'modern', 'a dropped legacy notification must not unpin or flip modern');
const unpinned = kernel().createConnection();
Expand Down Expand Up @@ -605,8 +644,8 @@ ok('D10 dropped notifications are countable — acceptance and rejection are dif
legacy.handleMessage({ jsonrpc: '2.0', id: 1, method: 'initialize', params: legacyInit() });
legacy.handleMessage({ jsonrpc: '2.0', method: 'notifications/initialized' });
assert.strictEqual(legacy.dropped(), 0, 'a legitimate legacy notification is accepted, not dropped');
const modern = kernel().createConnection();
modern.handleMessage({ jsonrpc: '2.0', id: 1, method: 'server/discover' });
const modern = kernel({ 'echo/params': { eras: ['modern'], handler: () => ({}) } }).createConnection();
modern.handleMessage({ jsonrpc: '2.0', id: 1, method: 'echo/params', params: { _meta: modernMeta() } });
modern.handleMessage({ jsonrpc: '2.0', method: 'notifications/initialized' });
assert.strictEqual(modern.dropped(), 1, 'the same notification on modern is a counted drop');
});
Expand Down
Loading