Revision history for Net-Async-MCP-Server

0.002     2026-10-18 23:30:05Z

  - StreamableHTTP: an Mcp-Param-* header whose body argument is null or
    missing no longer emits an 'uninitialized value' warning; the request
    is still answered 400 / -32020.

  - legacy_compat tools/call: params.arguments that is defined but not an
    object now returns -32602 'Invalid params: arguments must be an object'
    with the request id instead of being passed to the tool; missing
    arguments still default to {}.

  - tools/call: a tool result carrying its own resultType (e.g. MRTR
    input_required) now has isError normalized to a JSON boolean, like the
    rich-content branch; the tool's own hash is no longer mutated.

  - register_tool/register_resource/register_prompt croak when the key
    (tool/prompt name, resource uri) is already registered, instead of
    appending a second entry that tools/list advertised but nothing could
    call.

  - Stdio transport: responses go through an IO::Async::Stream when
    write_handle has an OS file descriptor, so a full stdout pipe no longer
    blocks the event loop; the stream is flushed at stdin EOF before
    handle_requests returns. In-memory handles keep the blocking print. The
    stream puts the handle into O_NONBLOCK (documented).

  - Documentation: handle() POD lists all seven dispatched methods;
    register_tool/list_tools document title, annotations and outputSchema;
    StreamableHTTP and Stdio comments corrected; README documents
    x-mcp-header enforcement.

  - prompts/get checks required arguments: a registered argument with a true
    "required" that is not defined in params.arguments returns -32602
    "Invalid params: missing required argument 'x'" before the prompt code
    runs.

  - tools/call: a tool result carrying structuredContent but no content
    array now gets a text content block with the JSON-serialized
    structuredContent (CallToolResult requires content; the spec asks for
    the backwards-compatible text block).

  - StreamableHTTP enforces x-mcp-header from the registered inputSchema: an
    annotated argument with a non-null value but no Mcp-Param-{Name} header
    is rejected with 400/-32020. Only chains of "properties" keys are
    walked (nested allowed); items/oneOf/anyOf/allOf/not/if/$ref are ignored.
    The value comparison uses the declared type, so a string-typed param
    compares as a string ("42" no longer matches "42.0"). New public
    Net::Async::MCP::Server->find_tool($name).

  - legacy_compat tools/call with non-object params returns -32602 Invalid
    params instead of dying out of handle(); a context without a transport
    key no longer warns.
  - tools/call: isError is now a JSON boolean on the 2026-07-28 wire (was
    1/0 as a number). The tool-error branch emits true; a rich-content result's
    own isError is normalised to true/false.
  - Breaking protocol migration to MCP 2026-07-28 (stateless, _meta-based)
    remains the default. initialize, notifications/initialized and ping return
    -32601 Method not found unless legacy_compat is enabled.
  - Add opt-in legacy_compat for Stdio-only MCP 2025-11-25 clients. Its
    initialize/ping/tools/list/tools/call responses use legacy result shapes;
    2026 requests keep their stateless resultType and _meta wire format.
  - Every request must carry _meta.io.modelcontextprotocol/protocolVersion
    and _meta.io.modelcontextprotocol/clientCapabilities; missing fields
    return -32602, an unsupported version returns -32022 with the server's
    supported list in data.supported.
  - Every successful result carries resultType and an
    _meta.io.modelcontextprotocol/serverInfo entry (injected by
    _jsonrpc_response, not by callers).
  - Add server/discover RPC — the new pre-flight RPC; clients can probe with
    it to learn the server's supported versions, capabilities and identity
    without any prior handshake.
  - tools/list returns the new wire shape: resultType "complete", tools
    using inputSchema (camelCase), and ttlMs / cacheScope caching hints.
  - tools/call wraps a tool's plain return value in a text content block;
    tool execution errors become isError: true results (the model can
    self-correct) while protocol-level errors (unknown tool) return a
    JSON-RPC -32602. A tool whose code returns a Future is awaited before
    the wrapping.
  - MRTR-ready call_tool: a tool that returns a hashref with
    resultType "input_required" and inputRequests / requestState is passed
    through unchanged, so multi round-trip requests work without further
    changes when a tool needs them.
  - tools/call: a tool may return a hashref carrying its own content arrayref
    and/or structuredContent (without a resultType) and have it passed through
    verbatim, so tools can emit image / audio / resource-link blocks and
    structured output instead of only a single text block.
  - tools/list now carries a registered tool's annotations and outputSchema
    through to the wire when register_tool was given them; both were silently
    dropped before.
  - Resources: register_resource / resources / list_resources / read_resource,
    with resources/list and resources/read dispatch. read_resource wraps a
    bare return in a { contents => [ { uri, mimeType?, text } ] } block, or
    passes through a hashref already carrying a contents arrayref; an unknown
    uri returns -32602. A code ref returning a Future is awaited before the
    wrapping; a dying code ref or a registered-but-codeless resource returns
    -32603 Internal error. Advertised in discover capabilities only when at
    least one resource is registered; capabilities are rebuilt per discover,
    so resources registered after the first discover are advertised too.
  - Prompts: register_prompt / prompts / list_prompts / get_prompt, with
    prompts/list and prompts/get dispatch. get_prompt wraps a bare return in a
    single user text message, or passes through a hashref already carrying a
    messages arrayref; an unknown name returns -32602. A code ref returning a
    Future is awaited before the wrapping; a dying code ref or a
    registered-but-codeless prompt returns -32603 Internal error. Advertised
    only when at least one prompt is registered; capabilities are rebuilt per
    discover, so prompts registered after the first discover are advertised
    too.
  - Server::configure now calls SUPER::configure, so unknown parameters
    surface through IO::Async::Notifier instead of being silently dropped.
  - register_tool now requires an inputSchema; the legacy input_schema key
    is accepted and normalised at registration time.
  - Drop the unused $context parameter from handle($request) and
    call_tool($name, $arguments). Under the stateless 2026-07-28 protocol
    per-request state arrives in the request's _meta block, which handle()
    validates itself; no caller ever passed a third argument.
  - New error codes -32020 (HeaderMismatch) and -32022
    (UnsupportedProtocolVersion) are wired into _jsonrpc_error
    (HeaderMismatch is raised by the Streamable HTTP transport, -32022 by
    handle()).
  - Stdio transport: handle_requests no longer reads through <STDIN>. It
    reads via an IO::Async::Stream registered as a child of the transport and
    waits on an EOF Future, so the caller still blocks until EOF while every
    other notifier in the same loop keeps running. If the transport is not in
    a loop yet, one is created and the transport added to it.
  - Stdio transport: new read_handle / write_handle parameters and accessors,
    defaulting to \*STDIN / \*STDOUT, so the transport can be driven from a
    pipe. Line handling is unchanged — empty lines skipped, invalid JSON
    answered with -32700, notifications write nothing, a final line without a
    trailing newline is still dispatched, EOF is the graceful-shutdown signal
    and the read handle is left open.
  - Stdio transport: handle_requests switches autoflush on for the write
    handle, once on entry. Without it a finished response could sit in Perl's
    block buffer while the client waited for it and the transport waited for
    the next request — a deadlock that only broke once 4-8K had piled up or
    the process exited. The write itself is still a blocking print.
  - Streamable HTTP transport for MCP 2026-07-28: new
    Net::Async::MCP::Server::Transport::StreamableHTTP module exposes a
    single HTTP POST endpoint (default /mcp), validates the spec's
    request-metadata headers (MCP-Protocol-Version, Mcp-Method, Mcp-Name,
    Mcp-Param-{Name}) against the JSON-RPC body, and answers each request
    as application/json or text/event-stream. Notifications return 202
    Accepted; header/body divergences return JSON-RPC -32020 HeaderMismatch.
    New runtime dep on Plack; new test file t/03-streamable-http.t.
  - Streamable HTTP transport: Mcp-Param-{Name} parity compares a body value
    by its JSON type instead of a raw string eq. A boolean is matched against
    'true'/'false' and a number numerically (so a '42.0' header agrees with an
    integer 42), fixing spurious -32020 HeaderMismatch rejections against the
    Net::Async::MCP client's spec-conformant header encoding.
  - Streamable HTTP transport: a spec-mandated JSON-RPC error now carries the
    HTTP status the spec requires instead of a blanket 200 -- -32601 answers
    404 Not Found and the -32020/-32021/-32022 protocol-parity codes answer
    400 Bad Request. A request missing io.modelcontextprotocol/clientCapabilities
    in its body _meta is rejected up front with 400 -32602 (a required field,
    checked in the transport so the shared handle() and Stdio are unchanged).
    Every other error handle() returns for a request that reached a real
    method -- an unknown tool/resource/prompt (-32602), an internal failure
    (-32603) -- still answers 200 with the JSON-RPC error in the body.
  - subscriptions/listen is not implemented in this release: it returns -32601
    in both transports, so Stdio and Streamable HTTP agree. Real server->client
    push delivery (subscription change notifications, logging, progress) is a
    future ticket.
  - Document the Streamable HTTP origin_allow default: an empty list means
    "allow any Origin", which does not by itself satisfy the spec's
    DNS-rebinding guard — set origin_allow to your known web origins for any
    browser-reachable deployment. Behaviour is unchanged; this is a
    documentation and SYNOPSIS clarification only.

0.001     2026-04-26 03:50:13Z

  - Initial release
  - Net::Async::MCP::Server - async MCP server base class
  - Built on IO::Async and Future::AsyncAwait
  - Handles JSON-RPC requests (initialize, tools/list, tools/call, ping)
  - Transport::Stdio for stdio-based MCP communication
