0.004     2026-08-15 16:13:38Z
  - Added an on_subscription_end event, so a caller is told when a
    subscription's stream ends on its own - the server closing it, the
    connection failing underneath it - instead of having to ask
    subscriptions_stop and get false back. The HTTP transport fires it with
    the subscription id the moment the stream ends without this client having
    ended it: subscriptions_stop and close deregister before they close, so an
    end the caller caused itself is not reported. The client forwards the event
    through %TRANSPORT_KEYS like on_notification, so a handler set there is
    called with the client. Only the HTTP transport can fire it; the InProcess
    and Stdio transports cannot carry a subscription
  - Fixed subscriptions_listen hanging on a stdio client instead of failing.
    MCP::Server >= 0.15 serves subscriptions/listen over stdio too, answering
    it with a notifications/subscriptions/acknowledged notification rather
    than a response, and the stdio transport ignored resolve_on_notification:
    the acknowledgement arrived at on_notification while the request waited
    forever for an answer that was not coming. The transport now refuses
    resolve_on_notification on the spot, so subscriptions/listen over stdio
    fails loudly with the reason instead of hanging
  - Added a client_capabilities option, so a caller can declare what the client
    supports instead of the hardcoded empty object every request used to carry.
    The default stays empty, which is what keeps a conforming server from
    sending input requests this client could not answer
  - Fixed list_tools, list_prompts and list_resources seeing only the first
    page of a paginated listing: a nextCursor in the result was ignored, and
    for list_tools that also left the header-parameter cache incomplete. All
    three now follow the cursor to the end. A server that repeats a cursor, or
    offers more than a hundred pages, fails the call loudly rather than
    returning what happened to arrive - a truncated list is indistinguishable
    from a short one, which is the same bug moved to a later page
  - Fixed subscriptions_listen being unusable over HTTP, which it was by
    construction: a subscription stream never carries a JSON-RPC response, so
    the request waited for something that could not arrive and died on the
    stall timeout. A server acknowledges by notification instead, and the
    client now tells the transport which notification settles the request. It
    resolves with the acknowledgement's own parameters - the subscription id
    and the notification kinds the server honoured - while the stream stays
    open and feeds on_notification. The new subscriptions_stop ends one by
    closing its stream, which is what unsubscribing is on this binding, and
    shutdown ends any that are left
  - Fixed every HTTP request waiting for its stream to close rather than for
    its answer. A server that holds the stream open after responding kept the
    caller waiting until the stall timeout; the response now settles the
    request the moment it lands. This is the same decoupling subscriptions
    needed, so both are one change
  - Fixed on_notification never reaching a Stdio client. The stdio transport
    delivers notifications, but the client built that transport without passing
    anything to it and forwarded a later configure only to an HTTP one, so the
    attribute did nothing for anyone not using HTTP. Which options reach which
    transport is now one table keyed by the class each option needs, so the
    HTTP-only ones still cannot reach a transport that would croak on them, and
    the InProcess transport - not a notifier at all - is excluded without
    being named
  - The default protocol version is now the revision this client builds
    requests for, spelled out here, instead of whatever the installed
    MCP::Constants names. Those were the same thing by coincidence; had MCP
    moved ahead, the client would have quietly put a newer version on requests
    shaped for the older one. lib/Net/Async/MCP.pm now loads no part of MCP at
    all, which suits a stdio or HTTP client, where MCP is only a recommendation
  - Renegotiation now picks from the revisions this client actually builds
    requests for, rather than from the installed MCP::Server's list of what it
    accepts. Those are different questions with a coincidentally identical
    answer today: let MCP ship a revision its server accepts and this client
    does not shape requests for, and the old reading would have renegotiated
    straight into it - a bug that would first appear on a dependency upgrade
  - Fixed the Stdio transport dropping every notification its server sends,
    the same gap the HTTP transport had before it read SSE incrementally, and
    delivered them to on_notification instead. Fixing the triage exposed a
    worse one alongside it: a message carrying an id but no result or error is
    a server-initiated request, and it went straight into the pending lookup -
    the server numbers its requests from its own counter, so its id 1 answered
    a caller's waiting id 1 with undef and a live tools/call silently received
    nothing
  - Fixed the Stdio transport leaving futures pending forever when it is
    removed from its loop while requests or a close are in flight. Removal
    takes the subprocess with it and unwatches the child, so the exit that
    would have settled them is never seen. They now fail, saying that rather
    than claiming an exit nothing observed, with the request case and the close
    case distinguishable from one another
  - A server refusing the client's protocol version is now answered by
    switching to one it offers and sending the request again, once, with the
    agreed version kept for everything that follows. Previously the refusal
    went straight to the caller. An offer this client cannot use leaves the
    server's own error untouched rather than replacing it. Note the reach of
    this: the client speaks exactly one revision's request shapes, so it can
    only ever switch back to that one - this catches a stale or mistaken
    protocol_version, it does not make the client multi-revision
  - on_notification set on the client is now invoked with the client as its
    first argument, like on_input_request, instead of with the transport that
    happened to receive the notification. A handler set directly on a transport
    object is still that transport's own event and still gets the transport
  - Added support for input required results, the current revision's way of
    letting a server ask the client something mid-request (MRTR, SEP-2322):
    instead of sending the client a request of its own, a server answers with
    resultType input_required, and the client fulfils what it asks and retries.
    The new on_input_request handler is invoked with the method and params of
    each ask and returns the answer, directly or as a Future; the retry then
    carries the answers under the server's own keys and mirrors requestState
    back untouched. An ask for a capability that was not declared in
    client_capabilities is refused as a server violation without ever reaching
    the handler, and an ask with no handler set fails saying so rather than
    handing back a result that would look like an empty answer
  - Added on_notification to the client, which only the HTTP transport had
    since it started reading SSE incrementally, so nobody using the client
    rather than the transport directly could reach it. The Stdio transport
    still drops what its server sends
  - A failed request now carries the server's JSON-RPC error object, not just
    a message about it. All three transports collapsed the error into a string,
    so a caller could not tell -32601 from -32602 without parsing prose, and
    error.data - where a server puts the versions it supports, or the scopes it
    wanted - never arrived at all. The failure now reads
    ( $message, 'mcp', $error ), which leaves the message in scalar context
    exactly as it was; the category is present only when there really is a
    server error object behind it, never for a transport-level failure
  - Fixed the HTTP transport throwing away everything a server sends before
    its answer. An SSE response was read to the end and only then parsed, and
    the parser kept the one event that looked like the response, so the
    notifications/progress and notifications/message a server may emit during a
    long tools/call were discarded unseen. The body is now parsed as it
    arrives, and a notification is delivered to the new on_notification event
    handler on the transport. Non-JSON-RPC events, keep-alive comments and an
    event torn across chunk boundaries - including in the middle of a
    multi-byte character - are handled where the old whole-body parser either
    could not see them or had no chance to get them wrong
  - Fixed the Stdio transport never being reclaimed. The stdout on_read and
    on_finish callbacks captured the transport strongly and live on the child
    streams of the IO::Async::Process the transport itself owns, closing a
    cycle no refcount breaks: a client that had been shut down and dropped left
    its subprocess's stream handles held for the life of the program. Both
    callbacks now hold the transport weakly and do nothing once it is gone
  - Added a headers option to the client and the HTTP transport, settable at
    construction and through configure, so an Authorization header can be sent
    at all: a server behind OAuth was previously unreachable, since the
    transport built its header list entirely by itself. Caller headers go on
    underneath the ones derived from the request body and a colliding one is
    dropped rather than sent alongside, because HTTP::Headers keeps a field
    given twice as two values of one header instead of letting the later win,
    which a conforming server sees as a header diverging from the body and
    rejects with -32020. Configuring them on a client that already joined a
    loop reaches the live transport, so a bearer token can be rotated
  - Added timeout and stall_timeout options for the HTTP transport, which ran
    without either: a hung server hung the caller indefinitely. stall_timeout
    defaults to 60 seconds and fires only when a request spends that long
    without a byte moving, so it catches the hung connection without touching
    a tools/call that legitimately runs for minutes. timeout, the limit on a
    whole request, deliberately has no default for that same reason
  - Fixed the InProcess transport reporting a correct server answer as
    "Invalid response from MCP server". A server object with a notification
    capable transport of its own answers subscriptions/listen with an
    MCP::Server::Subscription object rather than a JSON-RPC response, which
    this transport has nothing to stream over; it now says that instead of
    blaming the response
  - Added cancellation to the Stdio transport: cancelling the Future returned
    by send_request drops the pending entry and writes a
    notifications/cancelled naming that request, which is the stdio form of
    cancellation (on HTTP it is closing the response stream). Nothing is
    written for a request that is already answered or whose subprocess has
    exited. The callback holds the transport weakly, since the pending table
    holds the future and the future holds the callback
  - Fixed configure(protocol_version => undef) leaving the client without any
    protocol version: the fallback was guarded by a check against an already
    emptied hash and so never ran, after which every request carried a null
    protocolVersion in _meta and MCP::Server answered -32602
  - Fixed ping claiming liveness it had not checked. It now asks the transport
    via the new is_alive method and fails the Future when the transport can no
    longer carry requests, instead of always returning success
  - Added is_alive to all three transports: false for Stdio once the subprocess
    has exited or been closed, always true for InProcess and HTTP, which hold
    no connection state between requests
  - Fixed initialize autovivifying an empty _meta hash into the result it hands
    back to the caller when the server sends none
  - Fixed initialize still passing a top-level capabilities parameter left over
    from the old handshake: server/discover ignores params entirely and the
    client capabilities already travel in _meta, so it only put a second truth
    on the wire
  - Fixed the HTTP transport swallowing JSON-RPC errors that arrive with a
    non-2xx status. MCP servers render METHOD_NOT_FOUND with a 404 and a
    rejected _meta with a 400, so a subscriptions/listen error came back as a
    bogus "session expired" (dropping the session ID with it) and -32602 hid
    behind the HTTP status line. A JSON-RPC error body now wins over the
    status
  - Fixed double UTF-8 decoding in the HTTP transport, which made every
    non-ASCII text/event-stream response fail to parse: bodies are now read as
    bytes, which is what the JSON decoder expects
  - Removed the unused _initialized flag; the current protocol revision is
    stateless, so there is no handshake state left worth tracking
  - Removed the Mcp-Session-Id machinery from the HTTP transport: requests no
    longer carry the header, responses are no longer scanned for it, and
    shutdown no longer sends a DELETE to the endpoint, since close is now a
    no-op. SEP-2567 removed protocol sessions and the header in revision
    2026-07-28, the only revision this client speaks: a server must ignore the
    header and neither mint nor echo a session ID, and answers DELETE with 405,
    so none of it could still have an effect. A bare 404 is now reported as the
    HTTP error it is instead of a made up expired session. Talking to a legacy
    server that still wants the header is no longer possible through this
    transport
  - Fixed the HTTP transport being unusable against a conforming server of the
    current revision: it sent nothing but Content-Type and Accept, while
    2026-07-28 mirrors a request's metadata into HTTP headers so intermediaries
    can route on it without parsing the body, and a server must reject a
    missing or diverging one with -32020 (HEADER_MISMATCH). Every single
    request came back as that error. POSTs now carry MCP-Protocol-Version,
    Mcp-Method and, for tools/call, prompts/get and resources/read, Mcp-Name,
    each derived from the request body so that header and body cannot drift
    apart; values outside printable ASCII travel base64 encoded in the
    =?base64?...?= sentinel. Known limitation: the Mcp-Param-* headers for tool
    arguments annotated with x-mcp-header are still not sent, so a tools/call
    that passes such an argument stays rejected
  - Raised the MCP prerequisite to 0.15, the release that added server/discover
    and made 2026-07-28 the only supported revision, since the handshake this
    client speaks cannot work against anything older
  - Added HTTP::Message to the recommended prerequisites: the HTTP transport
    requires HTTP::Request at runtime and so far only got it by accident
    through Net::Async::HTTP, which is itself only a recommendation. Its tests
    now skip rather than die when it is missing
  - Fixed the HTTP transport documentation still naming protocol revision
    2025-11-25 while its own seealso link pointed at 2026-07-28
  - Added subscriptions_listen client method for the subscriptions/listen
    request, completing the set of current-revision client methods
    (list_tools/call_tool, list_prompts/get_prompt, list_resources/read_resource,
    and subscriptions/listen). It carries the standard _meta on every request
    and surfaces a server's METHOD_NOT_FOUND (e.g. when the server has no
    notification transport) as a failed Future.
  - Fixed obsolete Rev-2025-11-25 initialize protocol. initialize now sends the
    current revision's server/discover request and carries protocol version,
    client capabilities, and client info in _meta on every request, restoring a
    working handshake with MCP::Server >= 0.15 / Rev-2026-07-28 (which dropped
    the top-level protocolVersion initialize form and the initialize/ping
    JSON-RPC methods in favor of _meta and server/discover)
  - Added protocol_version accessor (defaults to MCP::Constants PROTOCOL_VERSION)
    and client-side _meta injection in Net::Async::MCP; transports remain dumb
  - server_info is now read from result._meta."io.modelcontextprotocol/serverInfo";
    server_capabilities from result.capabilities
  - ping is now a transport-level liveness no-op (the current MCP revision has
    no client-addressable JSON-RPC ping request); it returns success without
    sending a request that would fail against MCP::Server >= 0.15
  - Removed the notifications/initialized notification initialize used to send
    after the handshake request: it now sends server/discover and nothing else.
    SEP-2575 removed the initialize/initialized handshake in revision
    2026-07-28, the Streamable HTTP binding of that revision defines no
    client-to-server notification at all (notifications/cancelled, the only one
    left in the core protocol, exists on stdio), and MCP::Server discards
    notifications anyway, so over HTTP it was one extra POST per initialize
    with no effect. send_notification stays on all three transports as public
    API for callers, this client just no longer uses it
  - Fixed the HTTP transport dying instead of failing its Future when a 2xx
    body carries an "error" that is not a JSON-RPC error object. MCP::Server's
    own HTTP transport renders its refusals as {error => 'Method not allowed'}
    and {error => 'Invalid JSON'}, and a gateway in between may invent another
    shape, all of which the JSON and SSE paths read as a hash reference and
    died on with "Can't use string as a HASH ref". Both paths now check the
    shape like the non-2xx path already did and fail with the text the body
    held; a JSON-RPC error without a message no longer warns about an
    uninitialized value while interpolating it
  - Fixed send_notification on the HTTP transport reporting every completed
    POST as a delivered notification, 4xx and 5xx included, so a rejected
    notification looked sent. A 2xx still resolves the Future (202 Accepted
    with no body is the normal case), while a non-2xx now fails it with the
    same precedence as the request path: a JSON-RPC error body wins over the
    HTTP status line
  - Added the Mcp-Param-{Name} headers for tool arguments annotated with
    x-mcp-header, the last documented gap in the HTTP binding: a conforming
    server rejects a tools/call that passes such an argument without its
    header, so those tools were simply uncallable over HTTP. call_tool now
    resolves them client-side from the tool's input schema - which arguments
    are annotated and how their values are spelled is MCP semantics, only the
    wire form belongs to a transport - and hands the finished name/value pairs
    to send_request, which now takes optional binding hints as trailing named
    options and encodes them like Mcp-Name. list_tools caches the annotations
    per tool; a call_tool for a tool the client has not seen fetches the list
    once, and only on a transport that mirrors headers at all, which the new
    transport method mirrors_header_params answers (true only for HTTP). A
    failed lookup does not fail the call: it goes out without headers and the
    server decides. Values are formatted the way the server compares them,
    including a \0 or JSON::PP::Boolean false, which as a Perl value is true
    and would otherwise have put "true" in a header over a body saying false.
    An argument the caller left out gets no header, since a server rejects an
    unexpected one exactly as it rejects a missing one. Known limitation:
    list_tools ignores a nextCursor, so only the first page of a paginated tool
    list is known

0.003     2026-07-12 20:46:34Z
  - Fixed InProcess transport to pass a real MCP::Server::Context to
    handle() instead of a plain hashref, restoring compatibility with
    MCP::Server >= 0.10 (Context class) and >= 0.11 (OAuth scope checks),
    which call methods like has_scope on the context for most requests
  - Added Claude Code house rules (.claude/rules/net-async-mcp-rules.md):
    engineering discipline, delegation to the net-async-mcp-worker agent,
    a release-permission gate, a never-act-without-instruction policy for
    GitHub issues, and pointers to the karr skill (now hardlinked) for
    internal AI-to-AI coordination

0.002     2026-03-07 05:14:23Z
  - Added HTTP transport for remote MCP servers (Streamable HTTP spec)
  - Improved .gitignore with proper directory patterns, tar.gz, cover_db,
    Carton, and editor backup entries

0.001     2026-02-20 03:25:51Z
  - Initial release
  - InProcess transport for direct MCP::Server interaction
  - Stdio transport for external MCP server subprocesses
