Skip to content

v0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Aug 04:12
· 1 commit to main since this release

0.3.0 - 2026-08-13

Changed

  • 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-28 tool_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.