You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Breaking: command handles are now named command_id; kill_session is
now kill_command; retained output references use command:<command_id>:stdout|stderr. The former command session_id, kill_session, and session: output references are not accepted.
Commands and retained output are now owned by the workspace server rather
than by an individual Mcp-Session-Id. Multiple authenticated clients for
the same workspace can continue, read, or terminate a command after
reconnecting or creating a new MCP transport session using the command_id
returned by exec_command.
Closing or expiring an HTTP transport session no longer terminates workspace
commands. Commands are still bounded by the existing active-count, retained
output, byte, timeout, and TTL limits and are terminated when the workspace
server shuts down.
Breaking:notifications/cancelled no longer terminates the command the
cancelled request started. The notification is still accepted and, as
before, answered with no response. A command outlives the request that
started it and is shared by every client of the workspace, and the mapping
was keyed by the client's own JSON-RPC id, so two clients that both used id: 1 could cancel each other's commands. Terminate a command with kill_command; the reduced cancellation responsiveness is tracked in issue #48.
Breaking:get_default_cwd and set_default_cwd are removed and the
default catalog is now 18 tools. A relative path always resolves against
the workspace root, so there is no session-scoped working directory to set,
read, or lose on reconnect. Pass a workspace-relative path, or exec_command's workdir, to target a subdirectory. The read_file next_action continuation now repeats the workspace-relative path it was
given rather than one relative to a session cwd, and server_info no longer
reports default_cwd.
Tool descriptions now direct remote clients to pass explicit path/workdir
arguments and include concrete examples for patching and command
continuation.
Error text now names the error's category and whether it is retryable, and
tells a model not to repeat a call that cannot succeed. retryable and category were only ever in structuredContent, which most clients do not
forward to the model, so a permanent failure was indistinguishable from a
transient one.
COMMAND_NOT_FOUND from write_stdin, kill_command, and read_output now
explains that the handle expired or never existed, states the retention
window a finished command's output has, and names exec_command as the way
to recover. Retrying a dead handle is the single largest source of failed write_stdin calls.
kill_command now declares kill_wait_ms (hard-kill escalation wait,
default 2000 ms) in its input schema; previously the runtime honored it but
schema validation rejected any call that passed it.
read_output no longer accepts the undocumented command:<id>:full
reference form, which silently read stdout only. Use the per-stream command:<id>:stdout / command:<id>:stderr references.
Retained command output now keeps the earliest bytes per stream (a frozen
head segment, one eighth of the per-stream budget) in addition to the
rolling tail, so the command echo and first errors survive large outputs. read_output reports head_retained_bytes and evicted_gap_bytes.
server_info exposes an output_retention block naming the per-stream
retention budget (buffer_bytes_per_stream, head_bytes_per_stream). How
often that budget was actually hit is a runtime-wide measurement rather than
an answer to one client, so the eviction counters (evict_events, evicted_bytes_total) and omitted-read counters
(read_output_omitted_hits, poll_omitted_hits) are reported in the
telemetry session_end event instead.
exec_command and read_output tool descriptions now direct clients to
redirect very large output to a file and page it with read_file / search_text.
A method this server does not implement now returns -32601 before the
handshake as well as after it. Such a call previously returned -32002 Server not initialized, which tells a client to handshake and retry a method that
will never exist.
Breaking: HTTP is now stateless. The server no longer issues an Mcp-Session-Id, and a request that returns one from an older server is
served normally instead of being refused with -32001 Unknown MCP session.
Every request is answered by the one runtime that owns the workspace, so the
128-session ceiling and its 503, the session idle expiry, and the MCP-Protocol-Version-must-match-the-session check are all gone.
Breaking:DELETE /mcp now returns 405 with Allow: POST; there is no
session to terminate. DELETE is no longer advertised in the Allow, Access-Control-Allow-Methods, or server card transport.methods lists.
Breaking: the handshake is no longer an admission gate. tools/list, tools/call, and the other implemented methods are served whether or not
the caller sent initialize first, and -32002 Server not initialized is
never returned. initialize is now idempotent: each one negotiates a
version on its own and answers with it, so a repeat that names a different
supported version is answered with that version instead of -32600 Server is already initialized with a different protocol version.
Breaking:server_info reports supported_protocol_versions (every
version this server speaks, newest first) in place of protocol_version
(the one version a session had negotiated), and the server card at /.well-known/mcp.json reports supportedProtocolVersions in place of protocolVersion. Neither is a session-scoped value any more.
Runtime state a shared server exposes to concurrent requests is hardened:
the runtime directory (and the HOME, TMPDIR, and cache directories under
it) is resolved to the primary or fallback location exactly once, so a later
failure reports RUNTIME_DIR_UNWRITABLE instead of moving a running
command's directories, and the non-git diff fallback snapshots its patch
baselines under the patch lock.
The dev extra now installs the official MCP python SDK (mcp). The
compliance suite drives this server with it over both transports, which is
the only check in the suite that does not use a client we wrote ourselves.
Telemetry now measures the process rather than one client's handshake. A
session is activated by the first request or notification that passes
envelope validation — in either era, and before the method runs, so a first
call that fails still reports it — while ping never activates one, leaving
an HTTP health probe against an idle server silent. Client identity moves
with the request that carried it: initialize emits its own handshake
event with the negotiated version and the clientInfo it was given, a 2026-07-28tool_error carries the sanitized clientInfo of that
request, and no other event claims to know who is calling. consecutive_failures and the 20-error budget are runtime-wide across every
client, session_end adds per-era request counts and a server/discover
probe count, and the retained-output counters that server_info used to
report travel with it. Each protocol choice a process first serves is also
logged as one line on stderr, telemetry on or off. See docs/telemetry.md.
Behavior change:initialize no longer fails with -32602 when a client
asks for a protocolVersion this server does not speak. As the handshake
spec requires, the server now answers with an InitializeResult naming the
newest version it does speak (2025-11-25); a client that asks for a
supported version still gets that version back. Asking to handshake with 2026-07-28 downgrades the same way, because that protocol states its
version per request instead of negotiating one.
Behavior change: an HTTP request without an MCP-Protocol-Version header
is treated as 2025-11-25, the newest handshake version this server speaks.
The older spec suggests assuming 2025-03-26, which this server has never
spoken. The header value travels with the request as context and is available
to the runtime; nothing echoes it, records it, or acts on it, and no method
behaves differently for it.
The runtime contract is now docs/runtime-contract-v0.3.md, and docs/migration-0.3.md collects every breaking change
above with what to do about it. The v0.2 contract is kept, frozen, as the
0.2.x record.
Added
Support for MCP 2026-07-28, which serves a request without a handshake.
Such a request states its own protocol version in params._meta
(io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities are required, io.modelcontextprotocol/clientInfo is optional) and may call server/discover, ping, tools/list, and tools/call immediately. A _meta version this server does not speak is answered with -32022 and the
versions it does
(data.supported); a missing or mistyped required _meta field is answered
with -32602. Requests without that _meta key — including legacy requests
that carry _meta.progressToken, and every initialize — keep the
handshake behavior they had.
server/discover answers the probe a 2026-07-28 client sends instead of a
handshake, so such a client never has to send one: it reports the versions
this server speaks per request (["2026-07-28"] alone, since naming a
handshake-era version here would invite the client to put one in its _meta, where it is unsupported), the tools capability, and the same
workspace instructions initialize returns. Those instructions quote the
workspace's own instruction files, so the result carries ttlMs: 0 and cacheScope: "private" as tools/list does. A probe that states no
protocol version in _meta is a handshake-era request and is still answered
with -32601, which is what sends such a client to initialize.
Streamable HTTP serves 2026-07-28 as well, with the mirror headers
SEP-2243 requires. Such a request must repeat its _meta protocol version
in MCP-Protocol-Version and its method in Mcp-Method; tools/call, resources/read, and prompts/get must also repeat their subject
(params.name, or params.uri for resources/read) in Mcp-Name, either
literally or wrapped as =?base64?<payload>?=. Any header that contradicts
the body — including a 2026-07-28 version header on a request whose body
is a handshake-era one — is answered with 400 and the new -32020.
Handshake-era requests are not asked for these headers and are unaffected.
A 2026-07-28 request that fails now reports it in the HTTP status as well: -32601 is 404, and -32602, -32020, and -32022 are 400. Any other
code, -32603 included, stays 200 with the JSON-RPC error, which is also
what every handshake-era error keeps returning.
MCP-Protocol-Version accepts any version this server speaks. A header
naming an unknown version is still refused with 400 and -32600, and data.supported now lists both eras.
CORS preflight allows Mcp-Method and Mcp-Name, and no longer allows Mcp-Session-Id.
Results for 2026-07-28 requests carry resultType: "complete" and an _meta.io.modelcontextprotocol/serverInfo; tools/list and server/discover also carry the conservative cache hints ttlMs: 0 and cacheScope: "private" on the result root. Responses to handshake clients
are byte-for-byte what they were and never carry these fields.
Fixed
A repeated initialize on one persistent STDIO process is answered instead
of failing with -32600 Server is already initialized. Connectors that probe
for a newer protocol, fall back to the handshake, and then send initialize
again on the same process could not finish a tool scan at all (issue #39).
This shipped first in 0.2.3, which replayed the negotiated result; here there
is no handshake state to replay, so each initialize simply negotiates and
answers on its own, and a repeat naming a different supported version is
answered with that version rather than rejected.
Two clients patching the same file no longer lose an update. Each HTTP
session used to own a runtime with its own patch lock while the files they
wrote were shared, so a second patch could validate against a baseline it had
read before the first one committed and overwrite it silently. One runtime
now owns the workspace, so its lock covers every client: the later patch is
answered with a retryable conflict.
apply_patch no longer silently rewrites lines it was not asked to touch. It
split both the file and the patch with str.splitlines(), which breaks on \x0b, \x0c, \x1c, \x1d, \x1e, \x85, \u2028, and \u2029 as well
as on \n, and then rejoined with \n. Any file containing one of those
characters had it replaced by a newline by any patch, including a patch that
changed an unrelated line, and a context line containing one could never
match.
The number of newlines at the end of a file is now whatever the hunk says it
is. The trailing newline was captured from the file before applying and put
back unconditionally, so a hunk that added a final blank line had it removed
again and a hunk that removed the final newline had it restored. A file's
last line is now addressable like any other.
A context line that is empty is accepted as the empty context line it stands
for, instead of failing the patch with Invalid empty patch line. V4A writes
such a line as a single space, and model output and intermediate layers
routinely strip that trailing space. Patch text ending in more than one
newline is likewise accepted.