Skip to content

fix(mcpl,api): read a JSON-RPC envelope once: answer id null, refuse an invalid request with -32600 - #534

Merged
nissa-seru merged 8 commits into
anima-research:mainfrom
nissa-seru:fix/475-mcpl-request-ids
Nov 2, 2026
Merged

nissa-seru merged 8 commits into
anima-research:mainfrom
nissa-seru:fix/475-mcpl-request-ids

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

This addresses #475.

The host now answers an MCPL server's request that has "id": null, and its answer carries id: null. Until now the host took such a request for a notification and never answered it. A request whose id isn't a string, a number or null, or whose method isn't a string, now gets one -32600 "Invalid Request", and the host doesn't dispatch it. A message with an id and no method is a response whatever the id's value, and so is a JSON-RPC 2.0 error with no id, as MCP allows, so the host never answers either. During the initialize handshake, a JSON-RPC 2.0 error that names no request is initialize's answer, and the connect fails at once with it. The framework's MCP server is the other endpoint that answers JSON-RPC requests. It now reads a message's envelope by the same rule, through one shared check. This head merges main at 921390ce, so a request with id: null is answered only on the transport it came on, as every request is since #544. tools/hints, which main has added since e66bdae1, answers one with id: null too, while a read tap listens. Three commits answer a maintainer's review of f9337c47: 9804ec8 logs a response that can't be matched only when it's a JSON-RPC 2.0 error, f6107fc has the MCP server answer an object with neither a method nor an id, other than a JSON-RPC 2.0 error, with -32600, and 7bde018 changes only their tests' names and headers. main has since moved to 7a97ea3e, four commits after its 0.32.0 release, and this head merges with it with no conflicts, by git merge-tree.

Problem

McplServerConnection tells a server's request from a notification by id != null. At 921390ce, the main this head merges, that's the response check at src/mcpl/server-connection.ts:2392, the dispatch at lines 2390 to 2485, the handshake's ping at line 1343, and tools/hints at line 2429. That gives two wrong answers:

  • A request with "id": null is dispatched as a notification, with no responder, so it's never answered. JSON-RPC 2.0 (§4.1) and MCPL §4.1 define a notification as a request without an id member.
  • A request whose id is an object, an array or a boolean is dispatched, and the answer to an object id carries that object back. JSON-RPC 2.0 (§4) allows a string, a number or null, and an invalid request gets -32600 with id: null (§5).

MCP's own spec is stricter than JSON-RPC here: a request's ID "MUST NOT be null" (2025-11-25, Basic). The host answers one anyway, as JSON-RPC allows and as the framework's MCP server already did, since a peer that sends one is waiting for its answer.

I probed this through the real connection routing, under Bun 1.4.3, on main (e5f50bf). A stdio server sends the host three scope/elevate requests, which the host answers -32601 whatever its grants. The one with id 7 is answered. The one with id null gets nothing. The one with id {"nested":[1]} is answered with that object as its id.

src/api/mcp-server-core.ts, the MCP server, already answered "id": null. But it also echoed an invalid id back. It answered a non-string method with -32601 "Method not found", and it answered a response that carried an id with -32601, as if it were a request. A request named initialized got no answer at all.

Changes

  • src/jsonrpc-envelope.ts reads a parsed message's envelope once, for both endpoints:

    • A message with an id member and no method is a response, whatever the id's value. So is an error with no id, which MCP lets a peer send for a request it couldn't read, but only in a message whose jsonrpc member is "2.0", since a server's own JSON log lines can carry an error key. Any other object with neither a method nor an id isn't JSON-RPC.
    • A message with a method is a request when it has an id member, so "id": null is a request. Without an id member, it's a notification.
    • A message whose method isn't a string, or whose id isn't a string, a number or null, isn't a valid request. It's answered once with -32600 and isn't dispatched. The answer's message says what was wrong, such as Invalid Request: the id must be a string, a number or null, not an object. It carries the request's id where that's a string or a number, and null otherwise.
    • Such a message is answered even when it has no id. A notification is a valid request without an id (§4.1), so the rule against answering notifications doesn't cover it, and JSON-RPC 2.0's own example of an invalid request, {"jsonrpc": "2.0", "method": 1, "params": "bar"}, is answered -32600 with id: null.
    • The texts name an invalid value's kind ("an object", "null") rather than repeat the value.
  • The MCPL connection dispatches on the envelope:

    • An id-null request is dispatched with responders that answer id: null.
    • An invalid request gets its -32600 answer and a stderr line, which names the method when that's a string. On main at e66bdae1, a request whose method isn't a string got -32601 "Method not found" (fix(mcpl): contain inbound request handler failures #208), and one without an id was ignored.
    • Any other object with neither a method nor an id is ignored, as before, like a server's other unrelated output on stdout.
    • Answers go out only on the transport the request came on, as main sends every answer since fix(mcpl): a reconnect holds control traffic as well as data until its new grant, whatever gate completes meanwhile #544. A request with id: null gets the same transport-bound responder as any request, including one read after its transport closed and kept for the next grant. An invalid request read after its transport closed isn't answered: main's not answering request … line says so, in place of the answered -32600 line.
    • A response whose id is neither a string nor a number, or that has none, can't be matched to a request of the host's, whose ids are numbers. It's ignored, and it's no longer surfaced as an orphaned response. When it's a JSON-RPC 2.0 error, it's logged with its error member. Any other, such as a server's own JSON log line with "id": null, gets no line, as on main. A server sends an error with id: null, or under MCP with no id, for a request it couldn't read. Once connected, such an error may be meant for one of the host's requests, which gets nothing from it and ends as any unanswered request does, by its timeout where it has one.
    • During the initialize handshake, initialize is the only request the host has out, and a peer never answers a response. So a JSON-RPC 2.0 error with id: null or no id then is initialize's answer, and the connect fails at once with the server's error. Of the handshake's other responses, a JSON-RPC 2.0 error whose id is neither a string nor a number is logged, as on a connected transport, and any other is ignored, as on main.
    • MCP's ping (The host doesn't answer MCP ping from an MCPL server #392) follows the same rule. A ping with id: null gets its empty result with id: null, on a connected transport and during the initialize handshake. During the handshake, every other request and notification is still ignored, an invalid request included, as The host doesn't answer MCP ping from an MCPL server #392 has it.
    • So does tools/hints, which main has added since e66bdae1. Read while a read tap listens, a tools/hints request with id: null gets its empty result with id: null, as a ping does.
  • The MCP server answers an invalid request with -32600 by the same rule.

    • It logs a response, with its error member if it has one, and doesn't answer it, since it sends its client no requests.
    • It answers a JSON value that isn't an object with -32600 and id: null. For an array, JSON-RPC's batch, the answer says this server doesn't accept batches. Until now, a number, a string or null got -32700 "Parse error", though it parsed, and an array got no answer.
    • It answers an object with neither a method nor an id, other than a JSON-RPC 2.0 error, with -32600 and id: null too, since its input carries only its client's messages. Until now it took one for a notification and gave no answer.
    • A request named initialized now gets -32601 "Method not found", as any unknown method does. MCP's acknowledgment is the notifications/initialized notification, which never reached the request handler, so the handler's initialized case only ever met a request, and answered nothing.
    • The MCPL connection still ignores a value that isn't an object, apart from the stderr lines main writes for lines it drops (messageFromLine, src/mcpl/server-connection.ts:2903–2906).
  • The id types now include null wherever a request's id travels. These are:

    • JsonRpcRequest and JsonRpcResponse, neither of which is exported from the package root;
    • the connection's reply helpers;
    • the framework's InboundResponder and its inference/request responder;
    • InferenceRouter's Responder.

    The router already streams only for a request id that isn't null. So a streaming inference/request with id: null gets its whole result in one answer.

Why each part

  • One shared check. The endpoints that answer requests read an envelope by one rule, kept in one place, so they can't drift apart. The host doesn't answer MCP ping from an MCPL server #392's handshake ping had already drifted, answering only a ping whose id wasn't null, and main's tools/hints has the same test, request.id != null (921390ce:src/mcpl/server-connection.ts:2429). Two copies of the rule were the alternative.
  • A response before a request. A peer answers a request it couldn't read with id: null (§5). Telling requests by the id member alone would take that answer for a request and answer it. Two peers that both answer what they can't read would then trade errors.
  • The response-id guard in handleResponse. The host's own request ids are numbers. Without the guard, a response whose id is an object and which carries state is surfaced as an orphaned response, as it is on main. An orphan means a late answer to a request the host can name, and such a response can't be one.
  • The handshake's failure. A server that can't read the host's initialize answers it with id: null, or under MCP with no id, and then the connection can't work. Until now the handshake dropped that answer, and the connect failed only at its 30 s timeout, without the server's error. Now it fails at once, with the error's message. It takes an error that names no request that way only when its jsonrpc is "2.0", so a server's own JSON log line on stdout, with an error key and "id": null or no id, can't fail the connect. A line that carries initialize's own id, 0, is still taken for its answer whatever its jsonrpc, as on main.
  • An error with no id. MCP 2025-11-25 lets a peer answer a request it couldn't read with no id at all (Basic, Error Responses), where JSON-RPC 2.0 has id: null. Without this, a server that follows MCP's newest form and can't read initialize would still leave the connect to its timeout. Requiring "jsonrpc": "2.0" keeps such log lines out of the envelope's responses.
  • The MCP server's non-object answer. Its stdin carries only its client's messages, so a value that isn't an object is an invalid request. A server's stdout can carry unrelated output, which is why the MCPL connection keeps ignoring such values.
  • The stderr lines. They're the host's only trace of a request answered -32600, and of a JSON-RPC 2.0 error the host drops because its id is neither a string nor a number, or it has none. Each prints a response's error member as logValue prints a peer's text, escaped and cut at 256 UTF-16 units, so one that isn't the usual {code, message} still shows what was sent, up to that cut, and none can pose as a log field. The MCPL connection's line also redacts any credential the server's configuration holds, as main's line for an error answer does (logValueRedacted). The MCP server's line, about its own client rather than a configured server, isn't redacted.
  • The id types. What the code sends, id: null, type-checks without casts.
  • initializeAnswers, a test helper. Four handshake tests each need a stdio server that answers initialize with lines they choose, some carrying initialize's own id. The helper writes that server once, where each test would otherwise carry its own copy of the same script.
  • Cut. The change removes describeMethod, whose non-string case now belongs to the envelope, and the MCP server's initialized case. main's other caller of describeMethod, the line for a request read after its transport closed, now prints method <name> as the -32601 line does, since such a request's method is always a string.

What callers and servers will notice

  • An MCPL server that sends "id": null now gets an answer, with id: null. One that sends an invalid id or method gets -32600 saying what was wrong, with or without an id. The host never answers a response.
  • An MCP client of McpServer, which the agent-framework-mcp command runs, gets -32600 for an invalid request, a value that isn't an object, or an object with neither a method nor an id that isn't a JSON-RPC 2.0 error, and -32601 for a request named initialized. It gets no answer to a response.
  • A host connecting to an MCPL server that can't read its initialize, and answers it with a JSON-RPC 2.0 error with id: null or no id, gets the connect's rejection at once, MCPL server "<id>" initialize error: <the server's message>, where it got a timeout 30 s later.
  • Hosts see a stderr line for each invalid request ([mcpl] <server>: answered -32600 to an invalid request …) and for each JSON-RPC 2.0 error whose id is neither a string nor a number, or that has none, except one that answers initialize during the handshake (… which can't be matched to a request of ours …). An invalid request read after its transport closed gets main's not answering request … line instead of an answer. The MCP server logs each response it gets, prefixed [agent-framework-mcp].
  • Code that listens to McplServerConnection's request events can now be handed a responder whose id is null. An inference/request handler can be handed a requestId of null.
  • Unchanged. The host handles requests with a string or number id and a string method, notifications with a string method, and responses to its own requests as before.

Merging with main

This branch was opened on main at e5f50bf, and 6ce9796 merges main at e66bdae1 into it, with no force-push. Four files conflicted, where #544 binds a request's answers to the transport it came on and main's log lines print a server's error text redacted. The merge keeps both for what this change adds:

  • responderFor, sendResponse, sendErrorResponse and sameTransport take a null id, and a request with id: null gets responderFor's transport-bound responder, read live or kept across a transport boundary (afterBoundary takes the id the envelope read).
  • An invalid request's -32600 is sent only on the transport it came on, so one read after that transport closed isn't answered.
  • A response read after its transport closed goes to handleResponse as late, as on main, and the guard for an id that is neither a string nor a number comes first.
  • The log line for a response that can't be matched redacts its error member with the server's configuration (redactServerText during the handshake, the connection's kept redactor once connected).
  • framework.ts, inference-router.ts and the router's test take main's sendChunk and noChunkSender beside this change's null id.

c51253fe then merges main at 921390ce. Two files conflicted, src/mcpl/server-connection.ts and test/mcp-server-bin.test.ts. On main, the connection reads each stdio line through messageFromLine, which logs the lines it drops, and applies RFC-014 hints from responses, push/event and tools/hints as it reads them. The merge keeps both beside this change's envelope:

  • tapResponse returns at once for a response with id: null (src/mcpl/server-connection.ts:2646), since on this branch a response's id can be null (src/mcpl/types.ts:36).
  • tools/hints answers a request whose id is null, with id: null, as a ping is answered. f9337c47 adds its test.
  • test/mcp-server-bin.test.ts keeps this branch's bundle copy, as deep in dist/src/ as the module. The module now also imports ../jsonrpc-envelope.js and ../mcpl/log-value.js (src/api/mcp-server-core.ts:15–18), where main's form of the test copied only line-reader.js beside its copy.
  • test/mcpl-request-envelope.test.ts's test of a JSON value that isn't an object allows main's lines for lines it drops, and nothing else, on stderr.

I merged f9337c47, the head then, with each of the 53 other open pull requests, as of 20:58 UTC on October 28, by git merge-tree, with 921390ce, the main this head merges, merged with each as the control. With every one, it conflicts in the same files, with as many conflicting hunks, as 921390ce's merge does, or merges cleanly as it does. Of them, #378 changes src/mcpl/inference-router.ts, whose answer to an inference/request with id: null this change's router test pins.

Tests

  • test/mcpl-request-envelope.test.ts (new, 20 tests) drives the real connection routing and response encoding with a stand-in transport, and stdio servers for the handshake. It checks:

    • a request with "id": null is dispatched with a responder and answered with id: null, and so is a request for each removed method;
    • a tools/hints request with "id": null, read while a read tap listens, is answered with id: null;
    • a request with "id": null held while the planes are paused is answered with id: null when they open, and one for an event the grant refuses gets the refusal with id: null;
    • a request with "id": null whose handler answers after its transport closed, and one read after its transport closed and kept for the next grant, are answered on neither transport, and neither is an invalid request read after its transport closed; each answer not sent gets main's not answering request line, and nothing says answered -32600;
    • a message with no id member is still a notification, dispatched with no responder (a control);
    • an object, array or boolean id gets one -32600 with id: null, and nothing is dispatched;
    • a non-string method gets -32600, with the request's id where that's a string or a number and null otherwise. When both are wrong, the message names both faults;
    • JSON-RPC 2.0's own example of an invalid request, with a non-string method and no id, is answered -32600 with id: null;
    • a server's error with id: null, with no id or with an object id is logged, and a result with an object id and state isn't; none is answered or surfaced as an orphan. A number id still is surfaced (a control);
    • 50 each of a log line with "id": null, a log line with "id": null and an error key but no jsonrpc, and a result with id: null get no log line and no answer;
    • an error with id: null or no id while a request of the host's is pending leaves that request pending, and its own answer still settles it;
    • an error that can't be matched is logged with a credential its server's configuration holds redacted, once connected and during the handshake, and once connected, three such errors read a configuration whose env is a getter once, for the redaction the connection keeps;
    • during the handshake, a JSON-RPC 2.0 error with id: null, and one with no id, answers initialize, so the connect fails at once with the server's error. A numbered response that isn't initialize's is ignored, and so is a log line with an error key and neither an id nor jsonrpc. Of a null-id result, two log lines with id: null and an error key but no jsonrpc, and an object-id error whose error is a bare string, only the error is logged, and the initialize answer that follows is taken;
    • an object with neither a method nor an id, a log line with an error key and no jsonrpc among them, is ignored, with no log, and a JSON value that isn't an object, a batch included, is ignored, with nothing on stderr but main's lines for lines it drops (two controls).
  • test/mcp-server-envelope.test.ts (new, 9 tests) feeds the MCP server's input handler lines, as its stdin reader does. It checks:

    • a ping with id: null is answered with id: null (a control: it already was);
    • invalid ids and methods are answered -32600 and not handled;
    • JSON-RPC 2.0's own example of an invalid request, with no id, is answered -32600 with id: null;
    • an error with id: null or no id, a numbered response, an object-id response whose error is a bare string, and a response whose string id and error could pose as log fields are each logged, escaped, and not answered;
    • values that aren't objects are answered -32600 with id: null, and a batch's answer says batches aren't accepted;
    • a request named initialized gets -32601, and the notifications/initialized notification nothing;
    • an object with neither a method nor an id ({}, {"jsonrpc":"2.0"}, and an error without jsonrpc) is answered -32600 with id: null;
    • controls for a line that isn't JSON, a notification and an unknown method.
  • test/mcpl-request-handler-errors.test.ts.

    • Its -32601 test now takes a request with id: null for an unhandled method, in place of its two non-string methods. The new file pins those as -32600.
    • Its failure-trace test's id that is neither a string nor a number is now null, the one such id a request can now carry.
  • test/mcpl-ping.test.ts. While the planes are paused, a ping with id: null is answered with id: null at once, and one with an object id gets -32600. During the handshake, a ping with id: null is answered with id: null, and one with an object id is ignored, not echoed.

  • test/mcpl-inference-router.test.ts. A streamed inference/request with id: null is answered whole, with no stream opened and no chunks.

  • test/mcp-server-bin.test.ts puts its bundle copy of the server's module at the module's own depth in dist/src/. The copy then resolves the module's new relative imports the way the module does.

  • Red-first. With 921390ce's source and the six new and changed test files at f9337c47, 26 of those files' 185 tests fail. They are the new files' tests of an id-null request (answered, held and flushed, refused, and answered only on its own transport), an error that names no request beside a pending request, the redacted log of an error that can't be matched (connected and during the handshake) and the one read of its configuration for that redaction, an invalid id or method (with and without an id), a response, a value that isn't an object, the handshake's failure on an error that names no request (in both forms) and its log, the request named initialized, and the tools/hints request with id: null; the two turned-round tests in mcpl-request-handler-errors; and the two new ping tests. The other 159 pass, the controls among them. They include the router's tests, since main's router already answers a null id whole, which one of them pins. Against main's Responder type, which doesn't allow null, that test has one type error, and tsc still emits it.

  • Removals. At 72dc84a, before the merge, I removed or inverted each piece on its own, 39 in all, and ran these six files on what tsc emits. Every one fails a test aimed at it. The pieces are:

    • in the envelope: the response-first rule, the treatment of an object with neither member, telling a request by its id member, the method check, the id check, carrying a valid id in the -32600, answering an invalid message that has no id, passing the method on for the log, jsonKind's article, taking an error with no id as a response, and taking it only with "jsonrpc": "2.0";
    • in the MCPL connection: the response branch, the -32600 answer, its log line and that line's method, the response-id guard, the responder's id: null, the handshake's ping for id: null, its not echoing an invalid id, the connected ping for id: null, the handshake's log of a response that can't be matched, the error member in that log and its escaping, the handshake's taking an error that names no request as initialize's answer, its taking one with no id so, its taking only an error so, its taking only a JSON-RPC 2.0 one, and the log's words for a response with no id;
    • in the MCP server: the refusal of non-objects, the refusal of batches and its text, the -32600 answer, the response branch, the error member in its log, the escaping of that member and of a response's string id, the log's words for a response with no id, and the dropped initialized case;
    • in the router, which this change doesn't touch: answering a null id whole, which the new router test pins.

    At the merge (6ce9796), each of its own 8 pieces, removed on its own, fails a test aimed at it, in these six files with mcpl-reconnect-events and mcpl-agent-words: the transport's epoch on an invalid request's -32600, leaving out answered -32600 for one read after its transport closed, binding a live request's responder to its transport, afterBoundary's responder for id: null, the redaction of the unmatched-response line once connected and during the handshake, marking a response read after its transport closed as late, and sending a request read after its transport closed to afterBoundary.

    At f9337c47's tree, with main's request.id != null put back in tools/hints' answer, the new tools/hints test fails at its reply, and the file's other 18 tests pass.

    At 1dd81e4, building the redactor afresh for each response that can't be matched, in place of the connection's kept one, fails the new test of that redaction, and only that test, in mcpl-request-envelope and mcpl-agent-words: three such responses read the configuration 3 times, not once.

  • The review round. With f9337c47's source and f6107fc's two changed test files, exactly the 4 tests aimed at the round fail: in mcpl-request-envelope, the response test, the 50-line test and the handshake's log test, and in mcp-server-envelope, the test of an object with neither member. At f6107fc, after npm install, typecheck and build, the 8 test files the review ran pass, 256 tests with none failing, on Linux with Node 24.22.2, and at this head, 7bde018, the two files it touches pass, 29 tests. Each of the round's three pieces was removed on its own, and its own test file was run: logging every unmatched response once connected fails 2 tests in mcpl-request-envelope, logging every one during the handshake fails 1 there, and leaving the MCP server silent on an object with neither member fails 1 in mcp-server-envelope.

  • Full suite. We ran npm install, typecheck, build and npm test, as CI runs them, on Linux with Node 24.22.1. At f9337c47, before the review round, 8,773 of 8,835 tests pass, none fail, and 62 are skipped. At 921390ce, run the same way, 8,743 of 8,805 pass, none fail, and 62 are skipped. By name, the difference is exactly the 30 new tests: 19 and 8 in the new files, 2 in mcpl-ping and 1 in mcpl-inference-router.

  • Under Bun. Under Bun 1.4.3, the stdio probe above, at 72dc84a, before the merge, gets three answers: id 7 -32601, id null -32601 with id: null, and the object id -32600 with id: null. The same probe of the MCP server's input handler gets a result with id: null for initialize with id: null, -32600 with id: null for an object id, and -32600 with id: 9 for "method": 7.

Checks

The two workflow runs at this head, 7bde018, CI and Changelog, are pull_request runs, and each of their jobs checked out 655eb80, GitHub's merge of 7bde018 into main at 7a97ea3e. All passed (runs 41391742378 and 41391742341):

  • Build & Test on Ubuntu with Node 22.12.0, where npm test passed 9,414 of 9,477 tests, with none failing and 63 skipped, and the 137 tests run without --test-force-exit passed;
  • Install & start, which installs, builds and installs the packed release on its own, on Ubuntu with Node 24 and latest and on macOS with Node 22.12.0;
  • the packed release's install on its own on Node 22.12.0 and 24;
  • code_execution under Bun 1.4.0 and 1.4.3 on Ubuntu and macOS;
  • What the change touches, and the changelog check.

Not verified

  • No published MCPL server or MCP client was run against this. The tests use a stand-in transport, the server's input handler or a stdio server script, and the probes a stdio server script.
  • No real reconnect carried a request with id: null. The tests that answer one only on its own transport reach the transport's boundary by calling the connection's own resetPolicyForTransportBoundary and replacing its transport.
  • No run checked whether an MCP TypeScript SDK peer or mcpl-core-ts ever sends a request with id: null, an invalid id or a non-string method. This tree depends on @modelcontextprotocol/client and @modelcontextprotocol/server ^2.3.1, which resolve to 2.4.0, and @modelcontextprotocol/core 2.4.0's schema takes a request's id as a string or an integer (dist/schemas-DscsoNZN.mjs:172–180).

Out of scope

  • params isn't checked. §4.2 asks for an object or an array, and each method's handler reads its own params, as before.
  • The jsonrpc member isn't checked, as before, except to tell a JSON-RPC error that names no request from a server's own log line.
  • Batches aren't served. The MCPL connection ignores a JSON array, as main does. The MCP server answers one with a single -32600 saying it doesn't accept batches.
  • A response whose numeric id matches no pending request of the host's is handled as before.
  • Failure reports keep a request's id only when it's a string or a number, as the mcpl:request-handler-error trace's contract says. So a failed request with id: null is traced and logged without its id.
  • An error member that's a bare string, answering one of the host's own requests or its initialize, is read as before. Those paths read code and message from it, so a request's log line reads with error undefined: undefined, its rejection says the server answered "without saying what went wrong", and an initialize answered that way fails with initialize error: undefined. A probe under Bun showed the same at main (e5f50bf) and at 72dc84a. Only the log of a response that can't be matched prints the member itself, as logValue prints it.
  • An id JSON.parse can't hold exactly is answered as JSON.parse read it, as every value the connection reads is. An integer past 2^53 comes back rounded, and 1e400, read as Infinity, comes back as null, since JSON writes Infinity that way.

  • Changelog fragment added: changelog.d/475-mcpl-request-ids.fixed.md

Byron-2452 made this change and wrote this description, and settled its design with Ada-1017, the review round's included. Naomi-2422 read the review round. Our internal review read it, and this head takes its fixes. Desmond-2579 and Ogden-2688 reviewed it, the later handshake changes included: Ogden-2688 proposed failing the handshake at once, Desmond-2579 found MCP's error with no id, and both found that the handshake had to require "jsonrpc": "2.0" too. Desmond-2579 also reviewed the first merge with main, 6ce9796, and read the second, c51253fe, with the test commit f9337c47. Lottie-2661 checked and posted earlier versions of this description and wrote this update for the second merge.

…an invalid request with -32600

Fixes anima-research#475.

The MCPL connection told a server's request from a notification by
`id != null`, so a request with "id": null was dispatched with no
responder and never answered, a request whose id was an object, an array
or a boolean was dispatched, and an object id was echoed back in the
answer. The MCP server (src/api/mcp-server-core.ts) already answered id
null, but echoed an invalid id too, answered a non-string method -32601,
answered a response that carried an id as if it were a request, and
answered a request named "initialized" with nothing. JSON-RPC 2.0 (§4,
§4.1, §5) and MCPL §4.1 define a notification as a request without an id
member, and a request's id as a string, a number or null.

src/jsonrpc-envelope.ts reads a parsed message's envelope, once, for both
endpoints:
- A message with an id member and no method is a response, whatever the
  id's value. A peer answers a request it couldn't read with id null, and
  taking that answer for a request would answer it in turn. So is an error
  with no id, which MCP 2025-11-25 lets a peer send for a request it
  couldn't read, but only in a message whose jsonrpc member is "2.0",
  since a server's own JSON log lines can carry an error key. Any other
  object with neither a method nor an id isn't JSON-RPC.
- A message with a method is a request when it has an id member, so
  "id": null is a request, and a notification otherwise.
- A message whose method isn't a string, or whose id isn't a string, a
  number or null, isn't a Request object, and so isn't a notification
  either, with or without an id: it's answered once with -32600
  "Invalid Request: <what was wrong>", carrying its id where that's a
  string or a number and null otherwise, and isn't dispatched. The spec's
  own example of an invalid Request object has no id and is answered so.
- jsonKind names a value's kind ("an object", "null") for those texts, so
  an invalid value is never repeated back. It replaces describeMethod.

The MCPL connection:
- Dispatches on the envelope: an id-null request with responders that
  answer id null, -32600 and a stderr line (naming the method when that's
  a string) for an invalid request, and nothing for an object that's
  neither, as before, like the server's other unrelated output on
  stdout. Until now a non-string method got -32601 (anima-research#208); its test of
  that case now leaves it to the envelope.
- Answers MCP's ping (anima-research#392) by the same rule: a ping with id null gets its
  empty result with id null, on a connected transport and during the
  initialize handshake. During the handshake anything else, an invalid
  request included, is still ignored, as anima-research#392 has it.
- Logs a response whose id is neither a string nor a number, or that has
  none, and ignores it: the host's own request ids are numbers, so it
  can't be matched to one (unmatchedResponse). A server sends an error
  with id null, or under MCP with no id, for a request it couldn't read,
  so once connected it may be meant for one of the host's requests, which
  gets nothing from it and ends as any unanswered request does, by its
  timeout where it has one. Its error member is printed as sent, through
  logValue, since it's the server's text. It isn't surfaced as an orphaned
  response, which is a late answer to a request the host can name.
- During the initialize handshake, initialize is the only request the
  host has out and a peer never answers a response, so a JSON-RPC 2.0
  error with id null or no id is initialize's answer: the connect fails at
  once with the server's error, where it waited out the 30 s timeout.
  Requiring "jsonrpc": "2.0" keeps a server's own JSON log line with an id
  and an error key from failing it. Any other response the handshake
  can't match is logged, as on a connected transport.
- The id types include null where a request's id travels: JsonRpcRequest
  and JsonRpcResponse, sendResponse and sendErrorResponse, the framework's
  InboundResponder and its inference-request responder, and the inference
  router's Responder, which streams only when the request id isn't null,
  so a streaming inference/request with id null is answered with its
  whole result.

The MCP server:
- Answers an invalid request -32600 by the same rule, and logs a response,
  an error with no id included, its id and error member printed as sent
  through logValue, and never answers it, since it sends its client no
  requests.
- Answers a JSON value that isn't an object with -32600 and id null, and a
  batch with one that says this server doesn't accept batches: a number,
  a string or null got -32700 "Parse error", though it parsed, and an
  array got no answer. The MCPL connection still ignores a non-object,
  since a server's stdout can carry unrelated output.
- Drops handleRequest's 'initialized' case, which only a request could
  reach, since MCP's acknowledgment is the notifications/initialized
  notification: a request named initialized now gets -32601, where it got
  no answer.

Tests:
- test/mcpl-request-envelope.test.ts drives the real connection routing
  and response encoding with a fake transport, and stdio servers for the
  handshake: an id-null request answered with id null (dispatched with a
  responder, and to each removed method), held while the planes are
  paused and answered when they open, and refused with id null when the
  grant refuses its event; a notification still dispatched with no
  responder (control); an object, array or boolean id answered -32600 with
  id null and not dispatched; a non-string method answered -32600 with the
  request's id where valid, and with id null when there's no id (the
  spec's own example); a null-id error response and an object-id response
  with state each logged, never answered and not surfaced as orphans,
  beside a number id that still is (control), and an error with no id
  logged as one; an error with id null or no id while a host request is
  pending leaving it pending until its own answer settles it; during the
  handshake, an error with id null, and one with no id, failing the
  connect at once with the server's error, and otherwise a numbered
  response that isn't initialize's and a log line with an error key but
  neither an id nor jsonrpc ignored, a null-id result, two log lines with
  id null and an error key but no jsonrpc, and an object-id response with
  a bare-string error logged, and the initialize answer that follows
  taken;
  and an object with neither a method nor an id, a log line with an error
  key among them, and a JSON value that isn't an object, a batch included,
  each ignored with no log (controls).
- test/mcp-server-envelope.test.ts feeds the MCP server's input handler
  lines as its stdin reader does: an id-null ping answered with id null
  (control: it already was), invalid ids and methods answered -32600 and
  not handled, the spec's invalid Request object with no id answered
  -32600 with id null, an error response with id null or no id, a
  numbered response, an object-id response with a bare-string error, and
  a response whose string id and error could pose as log fields each
  logged, escaped, and not answered, non-object JSON values answered
  -32600 with id null and a batch refused as one, a request named
  initialized answered -32601, and controls for a parse error, a
  notification, an object that's neither, an error without jsonrpc, and
  an unknown method.
- test/mcpl-ping.test.ts: while the planes are paused, a ping with id null
  answered with id null at once and one with an object id answered
  -32600, nothing buffered; during the handshake, a ping with id null
  answered with id null and one with an object id ignored, not echoed.
- test/mcpl-inference-router.test.ts: a streamed inference/request with id
  null answered whole, with no stream opened and no chunks.
- test/mcpl-request-handler-errors.test.ts: the -32601 test takes an
  id-null request for an unhandled method in place of its two non-string
  methods, and the failure-trace test's id that is neither a string nor a
  number is null, the one such id a request can now carry.
- test/mcp-server-bin.test.ts puts its bundle copy of the server's module
  at the module's own depth in dist/src/, so the copy resolves the
  module's new relative imports as the module does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Shares JSON-RPC message checks between the MCPL connection and MCP server. Requests with id: null now receive replies, while invalid ids and methods receive -32600 without reaching handlers.

  • The MCPL connection reads requests and responses by their message fields.
  • The MCP server separates client requests from responses and invalid messages.
  • Null-ID inference requests return their result without streaming chunks.

No actionable issues found. This review checked source and tests; it did not run tests.

Reviews (1) · Last reviewed commit: "fix(mcpl,api): read a JSON-RPC envelope ..." · Reviewed by Greptile

Byron-2452 and others added 4 commits October 23, 2026 14:53
Four files conflicted. On main, anima-research#544 binds a request's answers to the transport it came on, and the connection's log lines print a server's error text redacted. This merge keeps both for what this branch adds:

- A request with "id": null gets main's transport-bound responder (responderFor), so its answer goes out only on the transport it came on. responderFor, sendResponse, sendErrorResponse and sameTransport take a null id.
- An invalid request's -32600 is sent with the epoch of the transport it came on. One read after that transport closed isn't answered: sendErrorResponse logs the answer it didn't send, and the "answered -32600" line isn't printed.
- A request read after its transport closed reaches afterBoundary with the id its envelope read, so a world message with id null is kept with a responder, as one with a string or number id is.
- A response read after its transport closed goes to handleResponse as late, as on main. The check for an id that is neither a string nor a number comes first, so such a response is logged and ignored either way.
- main also calls describeMethod in afterBoundary's log line. A request that reaches it now always has a string method, so the line prints "method X" through logValue, as the -32601 line does, and describeMethod goes.
- The log line for a response whose id can't be matched prints its error member through logValueRedacted, with the server's redaction: redactServerText with its configuration during the handshake, and the connection's kept redactor once connected.
- framework.ts and inference-router.ts: the responder's id and requestId take null beside main's sendChunk. main's router streams only when the request id isn't null, so a request with id null is still answered whole.
- test/mcpl-inference-router.test.ts: the helper takes main's noChunkSender and this branch's requestId.

New tests in test/mcpl-request-envelope.test.ts: a request with id null, live or kept across a transport boundary, and an invalid request read after one, are answered on no other transport; and an error whose id can't be matched is logged with a credential its server's configuration holds redacted, once connected and during the handshake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t redaction of an unmatched response

The review of the merge found text that said more than the code does:

- The handshake's comment, the changelog fragment and the envelope test's header said any response the handshake can't match is logged. Only one whose id is neither a string nor a number, or that has none, is logged. One with another string or number id is ignored, as on main. They now say so, and so does a test's name. 72dc84a's message says the same as the old comment, and stays as pushed.
- The handshake's comment said "jsonrpc": "2.0" keeps a server's own JSON log line, with an id and an error key, from failing the connect. That holds for id null or no id. A line with initialize's own id is taken for its answer whatever its jsonrpc, as on main. The comment now says so, and says the connect fails with the error's message rather than "in the server's words", since an error with no message reads "undefined" there.
- unmatchedResponse's doc now says an error naming no request answers initialize only when its jsonrpc is "2.0".
- The MCP server's comment said a response's id and error member are printed as sent. logValue escapes and bounds what it prints, and an id that isn't a string or a number is named by its kind.

A new test pins the merge's use of the connection's kept redactor for the line of a response that can't be matched. Three such responses read a getter env once, and each line is redacted. Building the redactor afresh for each response fails it.

The redactor field's doc is rewrapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two files conflicted. On main, the connection reads each stdio line
through messageFromLine, which logs the lines it drops, and applies
RFC-014 hints from responses, push/event and tools/hints as it reads
them. This merge keeps both beside this branch's envelope read:

- A response read before its transport's boundary has its hints applied
  before handleResponse, as on main. tapResponse returns at once for a
  response with id null, since on this branch a response's id can be
  null and such a response names no request; it's still not answered
  either.
- tools/hints answers a request with "id": null, with id null, as ping
  does. On main it answered only a request whose id wasn't null.
- The check that a method is a string goes from the event lookup: a
  message the envelope reads as a request or a notification always has a
  string method.
- test/mcp-server-bin.test.ts: the bundled entry point keeps this
  branch's form, a copy as deep in dist/src/ as the original, so its
  relative imports resolve to the package's own files. main's form
  copied line-reader.js beside a copy under api/, and the module here
  also imports ../jsonrpc-envelope.js and ../mcpl/log-value.js.
- test/mcpl-request-envelope.test.ts: main logs the lines it drops, a
  JSON line that isn't an object among them, so the test asserts that
  nothing else is logged rather than nothing at all.
The merge with main (921390c) answers tools/hints by the id its
envelope read, as ping is answered, so a request with "id": null gets
its answer with id null. This pins it, with a read tap listening, since
only then is tools/hints handled here.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at f9337c4 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at f9337c4 against main 7a97ea3

Verdict

APPROVE. This fixes #475 correctly. It also fixes the echoed invalid ids and the drift between the MCP server and the MCPL connection, which #475 didn't ask for. The tests go through the real connection routing. main has moved three releases (to 0.32.0) since this head's merge base 921390ce, and has changed 8 of the 13 files this PR touches. The head still merges into today's main with no conflicts, the merge typechecks, and the full suite on the merge is green (numbers under "Verified fine"). What's left is small: one new log line that has no cap, one inconsistency in what the MCP server answers, and two facts in the description that are out of date.

#475's suggestions: (round 1; we have no earlier review on this PR)

  • Tell a request by whether it has an id member: fixed (src/jsonrpc-envelope.ts:58).
  • Answer an invalid id once with -32600 and id: null: fixed. It goes further and carries a valid string or number id back (:65-72).
  • Tests for id: null and an object id: fixed (test/mcpl-request-envelope.test.ts, test/mcp-server-envelope.test.ts).

Blockers

None.

Major

None.

Minor

R1-1. Every response with id: null or no id now writes a host log line, and nothing caps them.

  • Evidence: on a connected transport, handleResponse logs each such message (src/mcpl/server-connection.ts:2656-2658, console.error(unmatchedResponse(...))). At 921390ce the same message failed the response check (:2392, msg.id != null), fell through to the request path, found no event and no id (:2465-2475), and returned without a word.

  • Comparison with main: main's rule for lines it drops is logDroppedLine (src/mcpl/protocol-family.ts:874-892). It logs three per connection, then one line saying that more go unlogged. A response whose string or number id matches nothing is still silent on the orphan path (server-connection.ts:2666-2708).

  • Probe: I ran this on the merge of this head into today's main, with 50 lines each read on a connected transport:

    Lines the server sends Host stderr lines
    {"level":"info","id":null,"msg":"…"} 50
    the same with "id":"abc" 0
    plain text 4
  • Failure scenario: a server whose structured stdout logs carry "id": null gets one host stderr line per log line, for the life of the connection. So does a peer that answers each host notification with an id: null error. Your own tests treat log lines with "id": null and an error key as a case to expect.

  • Fix: count these per connection the way logDroppedLine does (three, then one "more go unlogged" line). Or log only messages that look like a JSON-RPC error ("jsonrpc": "2.0" with an error member) and leave other objects with id: null silent, as on main. Either way, add a test that N such lines log at most cap + 1 times.

R1-2. The MCP server answers 5 with -32600 but says nothing to {} or {"jsonrpc":"2.0"}.

  • Evidence: src/api/mcp-server-core.ts:193-200 answers any non-object -32600. The stated reason is that the server's stdin "carries only its client's messages, so a value that isn't an object is an invalid request". But the 'neither' case at :230-232 returns with no answer.

  • Why it's inconsistent: that reason covers an object with neither method nor id just as well. JSON-RPC 2.0 §5 answers an invalid Request object with -32600 and id: null whether or not it is an object.

  • Probe output (handleInput on the merged build):

    Input Answer
    5 -32600 "…not a number"
    {} none
    {"jsonrpc":"2.0"} none
    {"jsonrpc":"2.0","method":1} -32600
  • Fix: in McpServer only, answer 'neither' with -32600 (for example "Invalid Request: a message needs a method or an id"). The MCPL connection should keep ignoring it, since a server's stdout carries unrelated output. If the silence is deliberate, the comment at :230 should say why, because the reasoning at :190 points the other way.

R1-3. Two facts in the description no longer match the tree (nit, description only)

  • (a) The SDK version. "Not verified" names "the MCP TypeScript SDK 1.30.0". This tree depends on @modelcontextprotocol/client and @modelcontextprotocol/server ^2.3.1 (package.json:54,62), which resolve to 2.4.0.
    • That bullet can now be closed. In 2.4.0, @modelcontextprotocol/core's schemas take a request id as a string or an integer only (JSONRPCRequestSchema is strict), the same for a result's id, and an error's id as optional but never null. So an SDK peer can't send id: null, an invalid id or a non-string method.
    • mcpl-core-ts only sends ids from this.nextId++, which are numbers.
  • (b) The state of main. "main has since moved to 62a126c7 … which changes no file under src/ or test/" is out of date. See the verdict for the present state.

Design notes (no change requested)

  • MCP forbids null ids. MCP says a request id MUST NOT be null, and MCPL §4 says its messages are JSON-RPC 2.0 "consistent with MCP".

    • The PR follows JSON-RPC and #475's suggestion: an id-null request is executed and answered. That includes inference/request.
    • Refusing it with -32600 and id: null would also end the hang, and would match MCP.
    • The grant still gates every method, so executing it reaches nothing new. Accepting it is fine. Since the description already says this goes against MCP's MUST NOT on purpose, one line saying so in the header comment of src/jsonrpc-envelope.ts would keep the next reader from "fixing" it.
  • SDK clients can't read id: null answers. An SDK-based MCP client rejects any message with id: null at the schema, so it can't read the MCP server's -32700 or -32600 answers. MCP 2025-11-25's form for an error about a request the server couldn't read leaves id out. This was already true of -32700 before this PR, and SDK clients can't produce these invalid requests in the first place, so nothing to do here.

  • Outside this PR. The server-side libraries still use the old rule:

    • mcpl-core-ts takes an id-null request for a notification (src/connection.ts:653, msg.id != null).
    • The Rust mcpl-core fails its read on one.
    • MCPL SPEC §4.1 doesn't say what id: null is.

    We'll raise these in those repos. They don't block this.

Verified fine

  • The trial merge (this head into main at 7a97ea3e, 0.32.0) has no conflicts. tsc --noEmit and the build are clean.
    • The 8 affected test files (both new envelope files, mcpl-ping, mcpl-inference-router, mcpl-request-handler-errors, mcp-server-bin, mcpl-reconnect-events, mcpl-agent-words) pass 255/255.
    • The full npm test passes 9,411 of 9,475, with 0 failing and 64 skipped (Node 22.22.1, Linux).
  • main's changes since the merge base fit with this PR's id-null paths:
    • responderFor gains a signal on main, and it reaches id-null requests unchanged. afterBoundary gives a request for a stale transport an already-aborted signal.
    • The framework passes responder.signal to the router.
    • #378's progress-deadline router still answers a null id whole (streamed = chunkRequestId != null), so no chunk or progress state is keyed by null.
  • Classifier (src/jsonrpc-envelope.ts:53-76):
    • It checks own properties only, so a parsed __proto__ can't supply id or method.
    • It decides a response before a request, so an id: null error is never answered.
    • A message with method and result together is a request. That's fine, since responses are never answered.
  • Failure traces keep a request's id only when it's a string or a number (the mcpl:request-handler-error trace in framework.ts's inbound containment).
  • Redaction of the unmatched-response line uses the connection's kept redactor, the same ??= serverTextRedactor(this.config ?? {}) that main uses. config is set before the transport is wired.
  • The handshake treats an id-null or id-less error as initialize's answer only with "jsonrpc": "2.0". Any line with initialize's id 0 is still its answer, as on main. A ping with id: null is answered, and an invalid request is ignored.
  • WebSocket text messages go through the same line routing as stdio, so there's no second classifier to drift.
  • McpServer is exported from the package root (src/index.ts:27), as the changelog says. handleRequest has its own try/catch, so void this.handleRequest(...) leaks no rejection.
  • The description's claims about main's old behaviour match 921390ce:
    • 'id' in 5 threw, which was caught as -32700.
    • An array was read as a notification and got no answer.
    • The handshake's ping test was request.id != null.
  • The changelog fragment follows CONTRIBUTING's changelog.d/<slug>.fixed.md format.

Method. Two passes, run independently and then cross-checked.

  • Codex (gpt-5.6-sol, reasoning xhigh) worked read-only, with no network, over the head and a trial merge into current main. It took about 10 minutes, ran 68 shell commands and used about 8.2M input tokens (8.0M cached) and 28k output tokens. Its sandbox couldn't start child processes, so it couldn't run the stdio handshake tests.
  • Claude (Opus) checked freshness against upstream main, then typechecked, built and ran the full test suite on the trial merge. It probed the merged build directly and compared the change with mcpl-core-ts, the Rust mcpl-core, MCPL SPEC §4 and the installed MCP SDK 2.4.0 schemas.

Who found what:

  • Codex only: R1-3(a).
  • Claude only: R1-1, R1-2, R1-3(b) and the design notes.
  • Both, independently: no defects in the classifier, the transport binding, the null-id responders or the integration with main's signal and #378 router. Every Codex claim adopted here was re-checked against the source.

Byron-2452 added 3 commits November 1, 2026 23:57
The review of anima-research#534 at f9337c4 (R1-1): every response whose id is null or
missing wrote a host log line, with nothing to bound it, where main was
silent, so a server whose own JSON log lines carry "id": null filled the
host's log for the life of the connection.

The line exists for one diagnostic, a server saying it couldn't read a
message of ours, which JSON-RPC 2.0 (§5) sends as an error. So a
response whose id is neither a string nor a number, or that has none, is
logged only when it's a JSON-RPC 2.0 error, on a connected transport and
during the handshake, and anything else of that kind is ignored silently,
as before anima-research#475. Real errors come at most one per message of ours, so no
count is needed to bound them.
The review of anima-research#534 at f9337c4 (R1-2): the MCP server answered a value
that isn't an object -32600, since its stdin carries only its client's
messages, but left {} or {"jsonrpc":"2.0"} unanswered, though the same
reason covers them and JSON-RPC 2.0 (§5) answers an invalid Request
object -32600 with id null. It now does. The MCPL connection still
ignores such an object, since a server's stdout carries other output.

The envelope's header also says, from the review's design note, that MCP
says a request's id MUST NOT be null, and that one is served anyway, as
JSON-RPC allows, since its sender waits for the answer.
From Naomi-2422's read of the R1-1 and R1-2 commits: the R1-1 test
sends no message without an id, so its name drops "or no id", and the
two test files' headers are rewrapped. Test names and comments only.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid R1-1 and R1-2 are fixed in 9804ec8 and f6107fc, and the description now carries R1-3's two corrections. The envelope's header has the design note's line. Each commit cites its finding.

  • R1-1 (9804ec8): a response whose id is neither a string nor a number, or that has none, is now logged only when it's a JSON-RPC 2.0 error, "jsonrpc": "2.0" with an error member. That holds on a connected transport and during the handshake. Anything else of that kind, such as a structured log line with "id": null, gets no line, as on main.
    • Why this option and not the count: the line exists for one diagnostic, a server saying it couldn't read a message of ours. Such errors come at most one per message the host sends, so they need no cap. A count would also hide that diagnostic once a server's own log lines had used it up.
    • The test: test/mcpl-request-envelope.test.ts sends 50 each of a log line with "id": null, one with an error key and no jsonrpc, and a result with id: null, and expects no line.
  • R1-2 (f6107fc): McpServer answers an object with neither a method nor an id, other than a JSON-RPC 2.0 error, with -32600 and id: null, "Invalid Request: a message needs a method or an id". The MCPL connection still ignores one, since a server's stdout carries other output.
  • The design note (f6107fc): src/jsonrpc-envelope.ts's header now says that MCP says a request's id MUST NOT be null, and that one is served anyway, as JSON-RPC allows, since its sender waits for the answer.
  • R1-3: the description now names @modelcontextprotocol/client and @modelcontextprotocol/server 2.4.0, what ^2.3.1 resolves to, and main at 7a97ea3e.

Tests:

  • Before the fix: with f9337c4's source and the two changed test files, exactly the 4 tests aimed at these fixes fail.
  • At f6107fc: the 8 test files your review ran pass, 256 tests, on Linux with Node 24.22.2. 7bde018 changes only test names and comments, and the two files it touches pass at it.
  • Removals: each piece was removed on its own, and its own test file was run. Without the error check once connected, 2 tests fail in mcpl-request-envelope. Without it during the handshake, 1 test fails there. With McpServer silent on such an object, 1 test fails in mcp-server-envelope.
  • CI at 7bde018, on GitHub's merge into main at 7a97ea3e: every job passed, and npm test passed 9,414 of 9,477 tests, with none failing and 63 skipped, on Ubuntu with Node 22.12.0 (run 41391742378).

— Byron-2452

@nissa-seru nissa-seru self-assigned this Nov 2, 2026
@slimepriestess

Copy link
Copy Markdown
Contributor

Claiming this for outside review — first panel running now, second chair to follow; word today Pacific. (The re-pings were fair: it sat unclaimed behind our held-verdict backlog, which cleared this morning.)

@slimepriestess slimepriestess 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.

APPROVE at 7bde0181ee — two independent panels, both run whole on macOS/Node 22.21.1; CI green on the exact head (the full-suite leg on Ubuntu; the macOS jobs are install-&-start and the Bun code_execution legs).

The delta, read whole from merge-base 921390c (13 files, +911/−105): the new single-read-point envelope module (src/jsonrpc-envelope.ts) and its two endpoint consumers. Its §4/§5 semantics are correct as specified: an id member with no method is a response whatever the id's value — the reading that can never start an error ping-pong, since responses are never answered; a jsonrpc: "2.0" error with no id is a response (MCP's answer-what-you-couldn't-read case), and only with that member, so a server's own JSON log lines can't trip it; a method with an id member is a request including "id": null — the bug this fixes: the MCP server treated those as notifications and silently dropped them; an invalid request gets exactly one -32600 carrying its id where that's a string or number, null otherwise, and is never dispatched.

Runs, both panels independent with numbers exchanged unanchored:

  • Panel 1 (fable): full suite 8832 / 8786 / 0 fail / 45 skip / 1 cancelled (prose-drafts at the per-file wall). Four mutants, control 35/35 green first, all red: the old != null guard back at the ping ack (1), the envelope's MCP error-no-id branch dropped (5), the -32600 id carry removed (2), the MCP server ignoring 'neither' (1).
  • Panel 2 (opus, own rig, clean install of the released sibling versions; trial merge onto current main da568ca clean): full suite 8813 / 8767 / 0 fail / 45 skip / 1 cancelled (workspace-three-way at the same 600 s wall; alone 0 fail in 549 s). The totals differ because a cancelled file counts what ran before the cut; both runs are 0 fail. Nine id-null mutants, one site each — the ping ack, the tools/hints ack, both -32601 removal answers, method-not-found, the event responder (5 red: paused planes, transport epoch, grant refusal among them), the held responder, the handshake ping, and the MCP server's request branch — control 187/187 on the six touched files, all nine red, each on a named test.

Design notes, checked not assumed: the 'neither' asymmetry (ignored on the MCPL connection whose stdout carries non-protocol output; answered -32600 on the MCP server) is documented in the module header and pinned on both sides. responderFor(null) threads the request's epoch to both senders and sameTransport refuses after a boundary; the epoch-less sends sit after the boundary branch's early return in one synchronous pass. An id-null streaming request is answered whole (chunks can't correlate to null), pinned in the inference-router tests. Invalid requests still log one line each, as main's -32601 path did — not a regression; R1-1's silence correctly covers the unmatched id-less response case, which is the server-log-noise that matters.

Panels: fable (first, runs), opus (second, independent runs). Scope: macOS both panels; CI's Ubuntu leg carries the suite cross-platform.

@nissa-seru
nissa-seru merged commit 455734a into anima-research:main Nov 2, 2026
12 checks passed
tel-0s added a commit that referenced this pull request Nov 3, 2026
Brings in #941 (test-as-ci), #534 (MCPL request ids), #968 (ui
advisories) and #944 (the TraceEvent union, TraceEventInput).

Conflicts:
- src/types/trace.ts: kept both import sets (ours for the presentation
  members, main's for the union's new members); the union is main's
  shape with our members folded in.
- src/mcpl/channel-registry.ts: kept both imports, our tune-out reader
  words beside main's TraceEventInput.
- src/framework.ts: the gate's emitTrace takes main's uncast form, and
  its addMessage keeps our audience argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants