@orkestrel/scaffold 0.0.94 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -258,11 +258,21 @@ from an absent header. Defaulting one is licensed only for a server that still s
258
258
  pre-`2025-06-18` clients, and this package does not:
259
259
 
260
260
  - `initialize` — legitimately headerless; nothing is negotiated yet. Accepted.
261
+ - An id-bearing legacy `ping` — answered with `{}` even before `initialize` returns.
262
+ With no session header, it passes through without minting a session, writing session state,
263
+ supplying a protocol header, or stamping the response. An unknown session id remains `404`.
261
264
  - A post-`initialize` legacy request on a **live session** — accepted, under the
262
265
  revision pinned at that session's `initialize`. That is a negotiated fact, not a
263
266
  default.
264
267
  - Anything else — nothing identifies the revision, so HTTP `400` + `-32020`.
265
268
 
269
+ The sessionless `ping` exception follows the pre-initialization allowance in the
270
+ [MCP 2025-11-25 lifecycle specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization)
271
+ and deliberately departs from the missing-session `400` recommendation in the
272
+ [Streamable HTTP session management specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management).
273
+ A client cannot hold the assigned session id before the `InitializeResult`. This exception
274
+ admits only a valid legacy `ping` request; notifications and modern-shaped pings do not qualify.
275
+
266
276
  A header naming an unsupported revision is a separate failure: HTTP `400` + `-32022`,
267
277
  carrying `{ supported, requested }`.
268
278
 
@@ -1376,6 +1386,10 @@ hook. Returning `undefined` from the hook continues into the ordinary live tool
1376
1386
  The keys are yours because they are how your own policy correlates each answer. A round asking
1377
1387
  nothing is refused, because it would seal state no retry could ever satisfy.
1378
1388
 
1389
+ `MCPInputOptions.clock` supplies epoch milliseconds for every continuation expiry read.
1390
+ It defaults to `Date.now`. Supply a clock beside `continuation` when the host owns the time
1391
+ source; tests can advance that clock during a provider call without replacing the host clock.
1392
+
1379
1393
  **`createMCPContinuation` is what protects the `requestState` echo, and it is required.**
1380
1394
  `MCPInputOptions.continuation` has no default: the carrier travels through a client that may
1381
1395
  have rewritten it, so the integrity of every binding inside it — principal, expiry, original id,
@@ -1984,6 +1998,30 @@ server-side one on `server.emitter`'s `error` event, a client-side one on
1984
1998
 
1985
1999
  ### Compose or remove the legacy protocol layer
1986
2000
 
2001
+ The optional `MCPServerOptions.handshake` hook gates only the legacy `initialize` response.
2002
+ `MCPServerInterface.handshake` exposes it, and `createMCPLegacy` passes it into
2003
+ `MCPLegacyOptions.handshake`. The `MCPHandshakeHandler` receives `MCPMethodOptions`, including
2004
+ the request's abort signal and optional caller context. On the binder carriers (stdio,
2005
+ WebSocket, and `MessagePort`), `ping`, forwarded legacy methods, and modern `server/discover`
2006
+ remain available while the hook waits. Omitting the hook preserves the dispatcher's answer
2007
+ bytes from 0.0.35. For the session-header change on refused HTTP initialization, see
2008
+ [HTTP transport](#http-transport).
2009
+
2010
+ Over HTTP, a separate legacy `ping` POST also completes while the hook waits, without a
2011
+ session id or negotiated protocol header. The session middleware passes that request through
2012
+ without publishing the pending initialization's candidate session.
2013
+
2014
+ An `MCPError` rejection preserves its code, message, and context as JSON-RPC `error.data`
2015
+ under the initialization request id. If the context cannot be serialized, the answer retains
2016
+ the code and message and omits `data`. Any other rejection emits `error` on the shared server
2017
+ emitter and answers `-32603` with `Server error`. A rejection after request cancellation is
2018
+ re-thrown; the stdio binder writes nothing and emits no error. The hook must observe its
2019
+ signal to stop pending work.
2020
+
2021
+ The [MCP 2025-11-25 lifecycle specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization)
2022
+ permits pings while initialization is pending and describes initialization error responses.
2023
+ The hook adds consumer readiness to that legacy exchange; it does not add a modern lifecycle gate.
2024
+
1987
2025
  The dated revisions are not a branch inside the server. They are a **decorator over it**.
1988
2026
  `MCPLegacy` wraps one `MCPDispatcherInterface` — the minimal `emitter` / `limit` / `dispatch` /
1989
2027
  `handle` surface a transport actually needs, and the one `MCPServerInterface` extends — and translates the
@@ -2076,6 +2114,7 @@ remaining survivor has its own consumer, and they are not the same one:
2076
2114
  | `inferEra` | Nothing inside `src`. It is the published era helper, and it reads `isMCPModernVersion` then `isMCPLegacyVersion` rather than restating either set. |
2077
2115
  | `inferRequestEra` | `MCPServer`'s `request` event and the HTTP ingress in `src/server/handlers.ts` — both report or route on the era a request's own structure selects, and neither reads a revision set. |
2078
2116
  | `isInitializeRequest` | Legacy server ingress: `src/server/middlewares.ts` mints and validates a session from it, and `src/server/inferers.ts` exempts a headerless `initialize` from the header demand. |
2117
+ | `isPingRequest` | Legacy server ingress: admits an id-bearing, non-modern ping without a protocol or session header. |
2079
2118
  | `MCPLegacyResult` | The unstamped result arm of `JSONRPCResponse` in `src/core/types.ts`, its guard `isMCPLegacyResult`, and the decorator's projection. |
2080
2119
  | `MCP_HANDSHAKE_VERSION` | Client-adapter egress in `src/core/MCPLegacyClientTransport.ts`, the legacy handshake anchor in `src/core/helpers.ts` and `src/server/inferers.ts`, and `SUPPORTED_LEGACY_PROTOCOL_VERSIONS`. |
2081
2120
  | `MCP_FALLBACK_VERSION` | `SUPPORTED_LEGACY_PROTOCOL_VERSIONS` plus an explicit `MCPLegacyClientTransportOptions.version` pin. |
@@ -2309,6 +2348,7 @@ A `Shape` cell holds the constant's declared type.
2309
2348
  | `isMCPResult` | function | Determines whether a value is one modern MCP result. |
2310
2349
  | `isMCPLegacyResult` | function | Determines whether a value is one legacy-era MCP result. |
2311
2350
  | `isInitializeRequest` | function | Determines whether a parsed value is an MCP `initialize` invocation. |
2351
+ | `isPingRequest` | function | Determines whether a parsed value is a legacy MCP `ping` request. |
2312
2352
  | `isMCPVersion` | function | Determines whether a value is a supported `MCPVersion`. |
2313
2353
  | `isMCPSubscriptionFilter` | function | Determines whether a value is an MCP `MCPSubscriptionFilter`. |
2314
2354
  | `isMCPConsumerFilter` | function | Checks whether a subscription filter leaves the built-in tools family to the server. |
@@ -2507,7 +2547,7 @@ An extended interface's name comes before `plus`, with the members it adds after
2507
2547
  | `MCPInputHandler` | type | `(context: MCPInputContext, options: MCPMethodOptions,) => MCPInputRound \| undefined \| Promise<MCPInputRound \| undefined>` | Decides whether the current `tools/call` still needs input from the client. |
2508
2548
  | `MCPPrincipalHandler` | type | `(request: JSONRPCRequest, options: MCPMethodOptions,) => string \| Promise<string>` | Derives the deployment-authenticated principal bound into signed request state. |
2509
2549
  | `MCPContinuationInterface` | interface | `{} plus seal, open` | Represents the host-neutral integrity and storage port for opaque MRTR continuation state. |
2510
- | `MCPInputOptions` | interface | `{ continuation, ttl, principal, selector }` | Configures the consumer policy for the server's multi-round-trip input mechanism. |
2550
+ | `MCPInputOptions` | interface | `{ continuation, clock?, ttl, principal, selector }` | Configures the consumer policy for the server's multi-round-trip input mechanism. The injected clock is trusted and must return finite epoch milliseconds. |
2511
2551
  | `MCPTaskStatus` | type | `'working' \| 'input_required' \| 'completed' \| 'failed' \| 'cancelled'` | Names the lifecycle state of one durable task. |
2512
2552
  | `MCPTask` | type | `{ taskId, status, statusMessage?, createdAt, lastUpdatedAt, ttlMs, pollIntervalMs? }` | Represents one durable task's wire snapshot — the payload a deferred `tools/call` answers with. |
2513
2553
  | `MCPTaskDetail` | type | `(MCPTask & { status: 'working' }) \| (MCPTask & { status: 'input_required', inputRequests }) \| (MCPTask & { status: 'completed', result }) \| (MCPTask & { status: 'failed', error }) \| (MCPTask & { status: 'cancelled' })` | Represents one task snapshot together with whatever its status carries — the shape `tasks/get` and a task notification report. |
@@ -2540,6 +2580,7 @@ An extended interface's name comes before `plus`, with the members it adds after
2540
2580
  | `MCPListenOptions` | interface | `{ signal, capacity? }` | Configures the per-subscription cancellation and bounded buffering policy. |
2541
2581
  | `MCPDispatchOptions` | interface | `{ signal?, caller? }` | Represents the per-request execution options every dispatched handler receives. |
2542
2582
  | `MCPMethodOptions` | interface | `{ signal, caller? }` | Represents the resolved per-request options one dispatched method receives. |
2583
+ | `MCPHandshakeHandler` | type | `(options: MCPMethodOptions) => Promise<void>` | Gates a legacy `initialize` response on consumer readiness. |
2543
2584
  | `MCPSubscriptionHandler` | type | `(notifications: MCPSubscriptionFilter, options: MCPMethodOptions,) => AsyncIterable<JSONRPCNotification> \| Promise<AsyncIterable<JSONRPCNotification>>` | Produces notifications for one honoured `subscriptions/listen` filter. |
2544
2585
  | `MCPSubscriptionOptions` | interface | `{ notifications, producer }` | Configures the server's built-in `subscriptions/listen` method. |
2545
2586
  | `MCPStream` | type | `AsyncGenerator<JSONRPCNotification, JSONRPCResponse, unknown>` | Represents a held-open modern result: each `yield` is a `JSONRPCNotification`; the `return` value is the terminating response. |
@@ -2551,10 +2592,10 @@ An extended interface's name comes before `plus`, with the members it adds after
2551
2592
  | `MCPServerEventMap` | type | `{ request, error }` | Represents the push observation surface of an `MCPServerInterface` — the dispatch moments a fire-and-forget observer (logging, tracing) subscribes to through `server.emitter.on`. |
2552
2593
  | `MCPLimitOptions` | interface | `{ message?, metadata?, keys?, state?, content?, subscriptions?, depth? }` | Configures the hostile-input and live-resource bounds for an MCP server. |
2553
2594
  | `MCPJSONLimitOptions` | interface | `{ bytes, keys?, depth }` | Limits applied by `isBoundedJSON` to one JSON value. |
2554
- | `MCPServerOptions` | interface | `{ on?, error?, identity, tools, resources?, prompts?, completion?, execution?, instructions?, cache?, input?, subscription?, task?, limit? }` | Options for `createMCPServer` — the server `MCPIdentity`, the live `ToolManagerInterface` it exposes, optional `instructions`, and the reserved `on` hooks. |
2595
+ | `MCPServerOptions` | interface | `{ on?, error?, identity, handshake?, tools, resources?, prompts?, completion?, execution?, instructions?, cache?, input?, subscription?, task?, limit? }` | Options for `createMCPServer` — the server `MCPIdentity`, the live `ToolManagerInterface` it exposes, optional `instructions`, and the reserved `on` hooks. |
2555
2596
  | `MCPDispatcherInterface` | interface | `{ emitter, limit } plus dispatch, handle` | Represents the minimal transport-facing MCP dispatch surface. |
2556
- | `MCPServerInterface` | interface | `MCPDispatcherInterface plus { identity, methods }` | Dispatches JSON-RPC 2.0 modern requests over a live `ToolManagerInterface`, with no transport coupling (a transport layer pumps strings through `handle`). |
2557
- | `MCPLegacyOptions` | interface | `{ dispatcher, identity }` | Represents the construction options for the removable legacy protocol decorator. |
2597
+ | `MCPServerInterface` | interface | `MCPDispatcherInterface plus { identity, handshake, methods }` | Dispatches JSON-RPC 2.0 modern requests over a live `ToolManagerInterface`, with no transport coupling (a transport layer pumps strings through `handle`). |
2598
+ | `MCPLegacyOptions` | interface | `{ dispatcher, identity, handshake? }` | Represents the construction options for the removable legacy protocol decorator. |
2558
2599
  | `MCPTransportInterface` | interface | `{} plus send, listen, closed, close` | Represents a duplex message channel an environment face provides to the pure engine — the one port `bindServer` and `bindClient` (`./helpers.js`) pipe an `MCPServerInterface` / `MCPClientInterface` over. |
2559
2600
  | `MCPMessageTransportEventMap` | type | `{ message, close, error }` | Lists the observable events of a `MCPMessageTransportInterface` — the moments the `MCPClientInterface` (and any tracer) subscribes to through `transport.emitter.on`. |
2560
2601
  | `MCPMessageTransportInterface` | interface | `{ emitter, session, duplex } plus start, send, close` | Pumps JSON-RPC messages to a peer and surfaces received messages on its `emitter`'s `message` event, with no knowledge of the protocol role on either side — a transport-agnostic MCP message carrier. |
@@ -2570,7 +2611,7 @@ An extended interface's name comes before `plus`, with the members it adds after
2570
2611
  | `MCPTaskClientInterface` | interface | `{} plus task, update, abort` | Reads, answers, and stops a durable task the peer created — the client half of the stable Tasks extension. |
2571
2612
  | `MCPClientInterface` | interface | `{ emitter, connected, version, transport, tasks } plus connect, discover, disconnect, tools, listen, call` | Connects to a remote MCP server over any injected `MCPMessageTransportInterface`, negotiates the modern wire revision, and exposes the server's tools as local `ToolInterface`s an agent can run. |
2572
2613
 
2573
- The `emitter`, `identity`, `methods`, and `limit` members of `MCPServerInterface` are
2614
+ The `emitter`, `identity`, `handshake`, `methods`, and `limit` members of `MCPServerInterface` are
2574
2615
  `readonly` data members in the Surface rows — its call-signature methods
2575
2616
  are documented under [Methods](#methods), and the registry `methods` exposes
2576
2617
  has its own method table there. Likewise the `emitter` /
@@ -2582,6 +2623,17 @@ members; their methods are under [Methods](#methods). The `id` member of
2582
2623
 
2583
2624
  ### HTTP transport
2584
2625
 
2626
+ For legacy initialization, `createMCPPostHandler` records the dispatch response in
2627
+ `MCPSessionState.initialization` before framing it as JSON or SSE only when consumer state is
2628
+ an object with `'session' in context.state`. `createMCPSession` stores and advertises a candidate
2629
+ session only when that response contains a result. An `initialize` that would mint a session
2630
+ stores none and advertises no `Mcp-Session-Id` when refused, including when the error travels
2631
+ in an HTTP 200 SSE event. A live session's header is returned unchanged, including on a refused
2632
+ `initialize`. A successful SSE initialization mints a session with or without a hook.
2633
+
2634
+ The [MCP 2025-11-25 Streamable HTTP specification, Session Management](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management)
2635
+ places the session header "on the HTTP response containing the `InitializeResult`".
2636
+
2585
2637
  The **Streamable HTTP transport** (`src/server`, through the `@src/server` barrel)
2586
2638
  mounts a transport-agnostic `MCPServerInterface` on the `@orkestrel/router` /
2587
2639
  `@orkestrel/server` spine as a route. `createMCPRoutes` returns the
@@ -2711,7 +2763,7 @@ accounting for inbound traffic subtracts a `('tools/list', 0, 'modern')` that pr
2711
2763
  `tools/list` under id `0` is not told apart on that event.
2712
2764
 
2713
2765
  Headerless
2714
- legacy `initialize` is accepted; a headerless post-initialize legacy request is
2766
+ legacy `initialize` and id-bearing legacy `ping` are accepted; another headerless legacy request is
2715
2767
  accepted only through a live session, whose pinned negotiated version the session
2716
2768
  middleware supplies; every other headerless request is HTTP `400` + `-32020`.
2717
2769
  `GET` / `DELETE` to the path fall through to whatever the router does with an
@@ -2723,8 +2775,8 @@ live in the session middleware).
2723
2775
  (`@orkestrel/server`); compose it with `router.use(createMCPSession())` in
2724
2776
  front of a session-agnostic `createMCPRoutes(mcp)`. It owns a closure
2725
2777
  `Map<string, { session, touched, version }>`, mints a session on an `initialize` POST
2726
- (`crypto.randomUUID()`), validates the `mcp-session-id` header on every other
2727
- legacy verb, and adds the resumable `GET` SSE stream — all native to this package.
2778
+ (`crypto.randomUUID()`), permits a legacy `ping` request without a session header,
2779
+ validates the session on other legacy requests, and adds the resumable `GET` SSE stream.
2728
2780
  A modern-shaped POST passes straight through without session lookup and ignores
2729
2781
  any `mcp-session-id`; the layer otherwise pins the negotiated legacy revision and
2730
2782
  supplies it on a headerless live-session request. The same default-on origin validation
@@ -2790,24 +2842,24 @@ A `Shape` cell holds the constant's declared type.
2790
2842
 
2791
2843
  #### Helpers
2792
2844
 
2793
- | API | Kind | Summary |
2794
- | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2795
- | `acceptsEventStream` | function | Checks whether the request's `Accept` header opts into a Server-Sent-Events response. |
2796
- | `allowsOrigin` | function | Checks whether an HTTP request satisfies the endpoint's origin gate. |
2797
- | `inferHeaderIssue` | function | Infers the first required MCP HTTP header a request's own body contradicts. |
2798
- | `inferSessionHeaderIssue` | function | Infers the protocol header issue an active legacy session's pinned revision diagnoses. |
2799
- | `inferHeaderTarget` | function | Infers the target one modern request's `Mcp-Name` header must carry. |
2800
- | `inferParameterRefusal` | function | Infers the refusal one `tools/call` earns for a `Mcp-Param-*` header the body contradicts. |
2801
- | `inferLegacyVersion` | function | Infers the legacy revision an `initialize` request negotiates. |
2802
- | `inferStatus` | function | Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body. |
2803
- | `readSessionHeader` | function | Reads the request's `mcp-session-id` header — the session id a stateful transport validates, or `undefined` when absent. |
2804
- | `readLastEventId` | function | Reads the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it reconnects to the resumable `GET {path}` stream, or `undefined` when absent. |
2805
- | `rejectUnknownSession` | function | Builds the stateful transport's "unknown session" rejection — an HTTP `404` carrying a JSON-RPC error body. |
2806
- | `sendEventStream` | function | Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per notification in order, then the terminating response — and end the exchange however the pump leaves. |
2807
- | `upgradeRequestPath` | function | Reads the path (without the query string) of a raw `node:http` protocol-upgrade request — the `createWebSocketServer` upgrade-path match. |
2808
- | `extractLines` | function | Folds one more chunk of raw stdio bytes into a newline-framed buffer — the shared line-framing step both stdio transports (client and server) read their inbound newline-delimited JSON-RPC messages through. |
2809
- | `writeLine` | function | Writes one line to a Node writable stream and waits for its completion callback. |
2810
- | `dispatchLines` | function | Decodes and delivers each complete newline-framed line onto a `MCPMessageTransportEventMap` emitter — the shared per-chunk dispatch step both stdio transports run their framed lines through: the server transport frames with `extractLines`, the client transport takes its lines from the process supervisor. |
2845
+ | API | Kind | Summary |
2846
+ | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2847
+ | `acceptsEventStream` | function | Checks whether the request's `Accept` header opts into a Server-Sent-Events response. |
2848
+ | `allowsOrigin` | function | Checks whether an HTTP request satisfies the endpoint's origin gate. |
2849
+ | `inferHeaderIssue` | function | Infers the first required MCP HTTP header a request's own body contradicts. |
2850
+ | `inferSessionHeaderIssue` | function | Infers the protocol header issue an active legacy session's pinned revision diagnoses. |
2851
+ | `inferHeaderTarget` | function | Infers the target one modern request's `Mcp-Name` header must carry. |
2852
+ | `inferParameterRefusal` | function | Infers the refusal one `tools/call` earns for a `Mcp-Param-*` header the body contradicts. |
2853
+ | `inferLegacyVersion` | function | Infers the legacy revision an `initialize` request negotiates. |
2854
+ | `inferStatus` | function | Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body. |
2855
+ | `readSessionHeader` | function | Reads the request's `mcp-session-id` header — the session id a stateful transport validates, or `undefined` when absent. |
2856
+ | `readLastEventId` | function | Reads the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it reconnects to the resumable `GET {path}` stream, or `undefined` when absent. |
2857
+ | `rejectUnknownSession` | function | Builds the stateful transport's "unknown session" rejection — an HTTP `404` carrying a JSON-RPC error body. |
2858
+ | `sendEventStream` | function | Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per notification in order, then the terminating response — and end the exchange however the pump leaves. |
2859
+ | `upgradeRequestPath` | function | Reads the path (without the query string) of a raw `node:http` protocol-upgrade request — the `createWebSocketServer` upgrade-path match. |
2860
+ | `extractLines` | function | Folds one more chunk of raw stdio bytes into a newline-framed buffer — the shared line-framing step both stdio transports (client and server) read their inbound newline-delimited JSON-RPC messages through. |
2861
+ | `writeLine` | function | Writes one line to a Node writable stream and waits for its completion callback. |
2862
+ | `dispatchLines` | function | Decodes and delivers each complete newline-framed line onto a `MCPMessageTransportEventMap` emitter — the shared per-chunk dispatch step both stdio transports run their framed lines through: the server transport frames with `extractLines`, the client transport frames the supervisor's stdout with Node's `readline`. |
2811
2863
 
2812
2864
  _This face declares no `decodeEvent`, `readEventStream`, or `buildResponseError`. Those SSE
2813
2865
  decoders and the response-error builder are host-independent and ship from `@orkestrel/mcp`; see
@@ -2831,7 +2883,7 @@ An extended interface's name comes before `plus`, with the members it adds after
2831
2883
  | `MCPSessionOptions` | interface | `{ capacity?, ttl? } plus clock?` | Options for the `MCPSession` entity — its folded replay log's capacity and per-event lifetime. |
2832
2884
  | `MCPSessionMiddlewareOptions` | interface | `{ path?, ttl?, session?, origin?, keepalive? } plus clock?` | Options for `createMCPSession` — the path the session middleware owns, the session idle time-to-live, and the per-session resumable event-log bound. |
2833
2885
  | `MCPSessionInterface` | interface | `{ id } plus attach, detach, push, replay` | Represents one MCP transport session — the per-session entity a `createMCPSession` middleware owns (the `MCPSession` entity), carrying the resumable server→client push channel with its bounded replay log folded in. |
2834
- | `MCPSessionState` | interface | `{ session? }` | Declares the `context.state` slice a `createMCPSession` middleware sets on a validated / minted request — a consumer's `TState` extends this so the downstream route handler can read `context.state.session` to `push` a server-initiated message onto the session's resumable stream. |
2886
+ | `MCPSessionState` | interface | `{ session?, initialization? }` | Declares the `context.state` slice a `createMCPSession` middleware sets on a validated / minted request — a consumer's `TState` extends this so the downstream route handler can read `context.state.session` to `push` a server-initiated message onto the session's resumable stream. |
2835
2887
  | `MCPSessionEvent` | interface | `{ id, message, timestamp }` | Represents one entry of an `MCPSessionInterface`'s folded replay log — a single pushed `JSONRPCMessage` tagged with the monotone event `id` the session assigned and the `timestamp` it was appended at (for the lazy-TTL replay window). |
2836
2888
  | `MCPSessionEntry` | interface | `{ session, touched, version }` | Represents the closure store entry a `createMCPSession` middleware keeps per minted session — the live `MCPSession` entity plus the epoch-ms instant it was last touched (the lazy-TTL sweep's idle clock, independent of the session's own replay-log TTL). |
2837
2889
 
@@ -2971,7 +3023,7 @@ case non-flowing with `readableFlowing === false`. Attaching a later `data` list
2971
3023
  resume that stream; the caller must call `resume()` before the listener receives data.
2972
3024
 
2973
3025
  `createStdioClientTransport` is the egress mirror — it builds one supervised
2974
- `@orkestrel/process` `Process` over `options.command` / `options.args` /
3026
+ `@orkestrel/process` `Supervisor` over `options.command` / `options.args` /
2975
3027
  `options.env`, and that supervisor spawns the child with
2976
3028
  `stdio: ['pipe', 'pipe', 'pipe']`: `stdin`/`stdout` carry the JSON-RPC channel
2977
3029
  and `stderr` is piped and retained as a bounded tail on the supervisor, not
@@ -2999,7 +3051,7 @@ replacing it: each named key overrides the inherited value, every unlisted key
2999
3051
  is still inherited, and the child therefore receives every secret this process
3000
3052
  holds. The server side frames its own input with `extractLines` (fold a
3001
3053
  raw chunk into complete lines + a carried remainder); the client side takes its
3002
- frames from the supervisor's `readline`-backed `lines` iterable instead. Both
3054
+ frames from Node's `readline` over the supervisor's stdout instead. Both
3003
3055
  decode through `dispatchLines` (decode + emit each complete line as `message` or
3004
3056
  `error`) — documented under [HTTP transport § Helpers](#helpers-1) because it
3005
3057
  lives in the shared `helpers.ts`.
@@ -3032,21 +3084,30 @@ landed draws no reply either. `DEFAULT_MCP_DELIVERY` is shorter than
3032
3084
  `DEFAULT_MCP_REQUEST_TIMEOUT`; that ordering distinguishes a default-bound
3033
3085
  undeliverable write from the later deadline for a peer that did not answer.
3034
3086
 
3035
- Closing the client runs the supervisor's bounded process teardown, which reaches the child's
3036
- terminal moment: the supervisor freezes `evidence`, ends `lines`, and settles the child's exit
3037
- together there. That ladder is signal-first — the supervisor terminates the child, then destroys
3038
- its `stdin` — rather than the stdin-close-and-wait the specification asks a stdio client for.
3039
- The posture and its cost are stated under
3040
- [Declared conformance gaps](#declared-conformance-gaps). The transport's line pump therefore needs no release of its own — the stream ends
3041
- under it. The wait for the child's streams is bounded by the supervisor's `drain` window, so a
3042
- descendant that inherited the child's stdout pipe cannot keep the transport's `close` call pending
3043
- past it. Inbound delivery ends at the call rather than at the stream's end: a line the supervisor
3044
- had already framed behind the one being delivered is dropped rather than emitted onto a transport
3087
+ Closing the client ends the child's input, waits up to `MCP_STDIO_GRACE` for native exit,
3088
+ and only then terminates a child that remains alive through the supervisor's bounded stop.
3089
+ This follows the [MCP 2025-11-25 lifecycle shutdown order](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#stdio).
3090
+ The specification leaves the duration open; `MCP_STDIO_GRACE` gives EOF cleanup half the
3091
+ existing 5,000 ms process signal grace, or 2,500 ms, before escalation gets its full window.
3092
+ This bounds disconnect without adding another full signal grace before termination. No transport option
3093
+ is needed by a consumer. The input flush shares that deadline, so a child that stops reading
3094
+ cannot hold closure open. A child that exits on input end within the bound receives no termination signal.
3095
+ That child must end its own child processes on input end; only escalation reaches its process tree.
3096
+ Escalation sends `SIGTERM` and then `SIGKILL` on POSIX; Windows uses `taskkill /F /T`.
3097
+
3098
+ If an `MCPClient` request `timeout` is shorter than the grace and the close outlasts that timeout,
3099
+ `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`. The close keeps
3100
+ running, and a later caller joins it while it remains pending.
3101
+
3102
+ Teardown reaches the child's terminal moment: the supervisor freezes `evidence`, closes the
3103
+ line reader, and settles the child's exit. The supervisor's separate `drain` window bounds the
3104
+ wait for inherited output pipes after native exit. Inbound delivery ends at the call rather than
3105
+ at the stream's end: a line already framed behind the one being delivered is dropped rather than emitted onto a transport
3045
3106
  whose teardown has begun. A `close()` issued while that teardown is running joins it rather than
3046
3107
  opening a second one, so it resolves only after the `close` event has fired.
3047
3108
 
3048
3109
  The tail frozen at that moment is what the supervisor had received by then, not the child's
3049
- complete output. On Windows the supervisor ends the tree with `taskkill /F /T`, which nothing in
3110
+ complete output. On Windows, after the input grace expires, the supervisor ends the tree with `taskkill /F /T`, which nothing in
3050
3111
  the child can intercept: a `SIGTERM` handler never runs there, so the bytes it would have written
3051
3112
  never exist. A child that ends on its own closes its `stderr` first, and that tail is complete.
3052
3113
  When the terminal moment arrived at the `drain` bound instead — a detached descendant holding the
@@ -3097,9 +3158,10 @@ const tools = await client.tools()
3097
3158
 
3098
3159
  A `Shape` cell holds the constant's declared type.
3099
3160
 
3100
- | Constant | Kind | Shape | Summary |
3101
- | ---------------------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3102
- | `DEFAULT_MCP_DELIVERY` | const | `10000` | Sets the default bound in milliseconds on one unconfirmed write to a stdio client transport's child `stdin` — the `delivery` a `createStdioClientTransport` caller who supplies none gets. |
3161
+ | Constant | Kind | Shape | Summary |
3162
+ | ---------------------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3163
+ | `DEFAULT_MCP_DELIVERY` | const | `10000` | Sets the default bound in milliseconds on one unconfirmed write to a stdio client transport's child `stdin` — the `delivery` a `createStdioClientTransport` caller who supplies none gets. |
3164
+ | `MCP_STDIO_GRACE` | const | `number` | Sets the bound in milliseconds for a stdio server to exit after the client ends its input. |
3103
3165
 
3104
3166
  #### Helpers
3105
3167
 
@@ -3227,7 +3289,8 @@ factory. `publish` registers each advertised tool through `registerTool` with th
3227
3289
  annotations, retains the `AbortController` whose abort is WebMCP's own unregistration path, and
3228
3290
  subscribes to the manager's own `emitter` so a later `add`, `remove`, or `clear` reaches the
3229
3291
  document registry without a second call; `adopt` reads `getTools` and wraps each
3230
- `RegisteredTool` as a tool whose `execute` runs `executeTool` with the caller's
3292
+ `RegisteredTool` as a tool, excluding `annotations.debugging: true` unless you pass
3293
+ `adopt({ debugging: true })`. Each tool's `execute` runs `executeTool` with the caller's
3231
3294
  `ToolContext.signal`; `destroy` aborts the registrations this handle made, releases that
3232
3295
  subscription, and leaves a name it never registered alone. WebMCP keys a registration by tool
3233
3296
  name per document, so a release takes whatever now stands under that name. See
@@ -3293,6 +3356,8 @@ A `Shape` cell holds the constant's declared type.
3293
3356
  | `DEFAULT_MCP_SERVER_NAME` | const | `'@orkestrel/mcp'` | Supplies the default server name `createScopeServer` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. |
3294
3357
  | `DEFAULT_MCP_SERVER_VERSION` | const | `'1.0.0'` | Supplies the default server version `createScopeServer` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. |
3295
3358
  | `WEBMCP_CHANGE_EVENT` | const | `'toolchange'` | Names the WebMCP registry event the bridge republishes as its own `change`. |
3359
+ | `WEBMCP_ACTIVATED_EVENT` | const | `'toolactivated'` | Names the WebMCP IDL `toolactivated` event the bridge republishes as `activate`. |
3360
+ | `WEBMCP_ABORT_EVENT` | const | `'toolcancel'` | Names the WebMCP IDL `toolcancel` event the bridge republishes as `abort`. |
3296
3361
 
3297
3362
  #### Helpers
3298
3363
 
@@ -3313,6 +3378,7 @@ through, and the registry guards. Each projection is the WebMCP direction of the
3313
3378
  | `collectWebMCPProjections` | function | Collects the WebMCP projection of every tool a registry advertises that WebMCP can carry. |
3314
3379
  | `isWebMCPRegistry` | function | Determines whether an unknown value is a WebMCP tool registry. |
3315
3380
  | `isWebMCPDocument` | function | Determines whether an unknown value is a document exposing the WebMCP tool registry. |
3381
+ | `isWebMCPToolEvent` | function | Determines whether an unknown value is a WebMCP execution event. |
3316
3382
 
3317
3383
  The SSE decoders `decodeEvent` and `readEventStream` are host-independent and ship from
3318
3384
  `@orkestrel/mcp`; see [Core § Helpers](#helpers).
@@ -3334,7 +3400,7 @@ An extended interface's name comes before `plus`, with the members it adds after
3334
3400
  | `ScopeServerOptions` | interface | `{ tools, name?, version? } plus accept?` | Options for `createScopeServer` — the live `ToolManagerInterface` to expose plus the optional server identity, mirroring `createMCPServer`'s `MCPServerOptions` (`@orkestrel/mcp`) but with `name`/`version` optional (defaulting to `DEFAULT_MCP_SERVER_NAME` / `DEFAULT_MCP_SERVER_VERSION`). |
3335
3401
  | `PageServerOptions` | interface | `{ tools, name?, version?, client? }` | Options for `createPageServer` — the live `ToolManagerInterface` to expose, the optional server identity, and the optional settings for the client half the pair owns. |
3336
3402
  | `PageServerInterface` | interface | `{ client } plus stop` | Represents one MCP server hosted inside the calling page — what `createPageServer` returns. |
3337
- | `WebMCPAnnotations` | interface | `{ readOnlyHint?, untrustedContentHint?, consequentialHint? }` | Describes a tool's observable effects as the WebMCP registry declares them. |
3403
+ | `WebMCPAnnotations` | interface | `{ readOnlyHint?, untrustedContentHint?, consequentialHint?, debugging? }` | Describes a tool's observable effects as the WebMCP registry declares them. |
3338
3404
  | `WebMCPDescriptor` | interface | `{ name, title?, description, inputSchema?, annotations? }` | Describes the members a WebMCP tool carries into the registry and back out of it. |
3339
3405
  | `WebMCPHandlerOptions` | interface | `{ signal }` | Carries the execution signal the WebMCP registry hands a registered tool's callback. |
3340
3406
  | `WebMCPExecuteHandler` | type | `(input: Readonly<Record<string, unknown>>, options: WebMCPHandlerOptions,) => Promise<unknown>` | Runs one registered WebMCP tool with the caller's input and the registry's signal. |
@@ -3343,13 +3409,14 @@ An extended interface's name comes before `plus`, with the members it adds after
3343
3409
  | `WebMCPRegisterOptions` | interface | `{ exposedTo?, signal? }` | Options for the WebMCP registry's `registerTool` — the exposure list and the unregistration signal. |
3344
3410
  | `WebMCPToolsOptions` | interface | `{ fromOrigins? }` | Options for the WebMCP registry's `getTools` — the origins whose tools are read. |
3345
3411
  | `WebMCPExecuteOptions` | interface | `{ signal? }` | Options for the WebMCP registry's `executeTool` — the caller's cancellation signal. |
3412
+ | `WebMCPToolEvent` | interface | `{ toolName }` | Carries the tool name a WebMCP execution event reports. |
3346
3413
  | `WebMCPRegistryInterface` | interface | `{} plus registerTool, getTools, executeTool, addEventListener, removeEventListener` | Represents the WebMCP tool registry a document exposes as `document.modelContext`. |
3347
3414
  | `WebMCPDocument` | interface | `{ modelContext }` | Describes a document that exposes the WebMCP tool registry. |
3348
3415
  | `WebMCPProjection` | interface | `{ tool, descriptor }` | Pairs one tool with the WebMCP descriptor a registry advertises for it. |
3349
- | `ModelContextEventMap` | type | `{ change: readonly [] }` | Reports the moments a WebMCP registry's contents changed. |
3416
+ | `ModelContextEventMap` | type | `{ change: readonly [], activate: readonly [name: string], abort: readonly [name: string] }` | Reports changes and execution events from a WebMCP registry. |
3350
3417
  | `ModelContextOptions` | interface | `{ document?, on?, error? }` | Options for `createModelContext` — the document to bridge and the emitter's initial wiring. |
3351
3418
  | `ModelContextPublishOptions` | interface | `{ origins? }` | Options for `ModelContextInterface.publish` — the origins each registration is exposed to. |
3352
- | `ModelContextAdoptOptions` | interface | `{ origins? }` | Options for `ModelContextInterface.adopt` — the origins whose tools are read. |
3419
+ | `ModelContextAdoptOptions` | interface | `{ origins?, debugging? }` | Options for `ModelContextInterface.adopt` — the origins and debugging tools to include. |
3353
3420
  | `ModelContextInterface` | interface | `{ emitter } plus publish, adopt, destroy` | Represents the bridge between a `ToolManagerInterface` and a document's WebMCP registry — what `createModelContext` returns. |
3354
3421
 
3355
3422
  _This face declares no `HTTPClientTransportOptions`. It is host-independent and ships from
@@ -3849,8 +3916,8 @@ the Node one also destroys an upgrade request still on the wire.
3849
3916
  `WebSocketServerTransport` unsubscribes before it runs the close handshake, so a
3850
3917
  frame already in flight cannot re-emit on a
3851
3918
  transport that has closed. `StdioClientTransport` stops its own line
3852
- dispatch at the call, terminates its child through the supervisor's bounded group
3853
- kill, and tears the supervisor down within the `drain` bound that caps a
3919
+ dispatch at the call, ends the child's input, waits up to `MCP_STDIO_GRACE` for exit,
3920
+ and then terminates any remaining child through the supervisor. Teardown uses the `drain` bound that caps a
3854
3921
  descendant-held stdout pipe.
3855
3922
  `StdioServerTransport` removes the listeners it put on `input` and `output`, rejects pending
3856
3923
  sends, preserves the caller's flowing or non-flowing state and listeners, and does not destroy
@@ -4081,7 +4148,8 @@ page.stop() // a repeat releases nothing further
4081
4148
  The WebMCP bridge `createModelContext` returns, implemented by `ModelContext`. `publish` sends
4082
4149
  this page's tools out to `document.modelContext`; `adopt` brings that registry's tools back as
4083
4150
  `@orkestrel/tool` tools. The `emitter` data member republishes the registry's `toolchange` as
4084
- `change`. Subscribe through `emitter.on`.
4151
+ `change`, `toolactivated` as `activate`, and `toolcancel` as `abort`. The execution events
4152
+ carry the tool name. Subscribe through `emitter.on`; the `destroy` method releases each registry listener.
4085
4153
 
4086
4154
  `publish` takes a snapshot **and** subscribes. The snapshot is taken when `publish` is called,
4087
4155
  before the work queues behind an earlier publication, so a manager mutated while a call waits
@@ -4158,15 +4226,21 @@ manager contains a throwing handler and reports `ToolFailure.error`, which is te
4158
4226
  value that was thrown no longer exists to forward and a foreign caller receives a fresh
4159
4227
  `Error` whose `message` is that text.
4160
4228
 
4229
+ The `adopt` method excludes a tool carrying `annotations.debugging: true` unless you pass
4230
+ `adopt({ debugging: true })`. A false or omitted hint leaves the tool included either way;
4231
+ the `origins` option continues to filter which registrations the registry returns.
4232
+
4161
4233
  **An adopted tool validates nothing.** It advertises the foreign `inputSchema` as its
4162
4234
  `parameters` and forwards the arguments it is given. Compiling that schema into a contract
4163
4235
  would let one page's unreadable or hostile schema refuse the whole `adopt` call, and the
4164
4236
  arguments reach a handler in another document that has to validate them anyway.
4165
4237
 
4238
+ The bridge exposes these methods.
4239
+
4166
4240
  | Method | Returns | Summary |
4167
4241
  | --------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
4168
4242
  | `publish` | `Promise<void>` | Registers every tool the manager holds at this moment, then follows it. |
4169
- | `adopt` | `Promise<readonly ToolInterface[]>` | Reads the document's registered tools as locally executable tools. |
4243
+ | `adopt` | `Promise<readonly ToolInterface[]>` | Reads the document's registered tools as locally executable tools, excluding debugging tools unless requested. |
4170
4244
  | `destroy` | `void` | Aborts every registration this handle made, stops following the tool registry, and releases the emitter — idempotent. |
4171
4245
 
4172
4246
  This example publishes a registry to a document that exposes WebMCP, reads the document's own
@@ -4202,6 +4276,9 @@ the bridge is the supported path, and it is what owns the registration lifetime.
4202
4276
  types it `Promise<DOMString>` while the specification's own README sample answers the MCP
4203
4277
  content shape `{ content: [...] }`. See [WebMCP parity](#webmcp-parity) for that row.
4204
4278
 
4279
+ The subscription pair accepts `toolchange`, `toolactivated`, and `toolcancel`; the execution
4280
+ events carry `WebMCPToolEvent.toolName`. The registry exposes these methods.
4281
+
4205
4282
  | Method | Returns | Summary |
4206
4283
  | --------------------- | ------------------------------------------ | --------------------------------------------------------------------- |
4207
4284
  | `registerTool` | `Promise<void>` | Registers one tool, resolving when the registry has accepted it. |
@@ -4782,6 +4859,12 @@ closed — while ordinary upstream completion closes the response without invent
4782
4859
  - [Client-side durable tasks and the absent poll loop](../tests/src/core/MCPTaskClient.test.ts)
4783
4860
  - [A task transition filtered, stamped, and carried to a subscribed client](../tests/src/core/MCPClient.test.ts)
4784
4861
  - [What the shared HTTP client transport owes on release, on headers, and on a non-success reply](../tests/src/core/transports/HTTPClientTransport.test.ts)
4862
+ - [The browser bridge to the WebMCP registry, published, adopted, and republished against the IDL-faithful double](../tests/src/browser/ModelContext.test.ts)
4863
+ - [The browser transports, scope server, page server, and feature detection composed against the Node-face servers](../tests/src/browser/factories.test.ts)
4864
+ - [The WebMCP projections and descriptor matching a bridge reconciles against](../tests/src/browser/helpers.test.ts)
4865
+ - [The WebMCP registry, document, and tool-event narrowing guards](../tests/src/browser/validators.test.ts)
4866
+ - [The message-port transport contract over a real `MessageChannel`](../tests/src/browser/transports/MessagePortTransport.test.ts)
4867
+ - [The WebSocket client transport releasing its socket on close and rejecting a send the channel cannot carry](../tests/src/browser/transports/WebSocketClientTransport.test.ts)
4785
4868
  - [The server face composed end to end over a real `node:http` listener](../tests/src/server/integration.test.ts)
4786
4869
  - [HTTP response lifecycle composition](../tests/src/server/HTTPDisconnect.test.ts)
4787
4870
  - [HTTP handler integration](../tests/src/server/handlers.test.ts)
@@ -4906,28 +4989,33 @@ this package serves over a wire, so each surface it defines is compared here aga
4906
4989
  package publishes, and every row ends implement, retain, or exclude with the source it was read
4907
4990
  from. `G5c` is the verbatim WebIDL, samples, and chromestatus reading taken from the
4908
4991
  [WebMCP specification](https://webmachinelearning.github.io/webmcp) on 2026-09-15; `G5` is the
4909
- surrounding research context. `createModelContext` is where the implemented rows live.
4910
-
4911
- | WebMCP surface | Ours | Verdict | Source |
4912
- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
4913
- | `registerTool(tool, { exposedTo, signal })` | `ModelContextInterface.publish(tools, { origins })` | implement — `origins` becomes `exposedTo`, and the retained controller's abort is the unregistration path | `G5c` § 1, § 2 |
4914
- | `getTools({ fromOrigins })` | `ModelContextInterface.adopt({ origins })` | implement — `origins` becomes `fromOrigins`, and each `RegisteredTool` comes back as a `ToolInterface` | `G5c` § 1 |
4915
- | `executeTool(tool, input, { signal })` | an adopted tool's `execute(args, context)`, and `ToolManagerInterface.execute` on the other side | implement — the caller's `ToolContext.signal` becomes WebMCP's `signal` in both directions | `G5c` § 1 |
4916
- | `toolchange` event | `ModelContextEventMap.change` | implement — the registry names no changed tool, so ours is a bare signal and a listener re-reads `adopt` | `G5c` § 2 |
4917
- | Registry change events on our own tool registry | `ToolManagerEventMap` (`add`, `remove`, `clear`), followed by `publish` | implement — `@orkestrel/tool` publishes registry events, so `publish` subscribes to the manager's `emitter` and the document registry tracks the tool registry with no second call | `G5c` § 2 |
4918
- | `inputSchema` (JSON Schema) | `ToolDefinition.parameters`, derived from `ToolOptions.contract` | implement, exceeds in the publish direction — a published tool carries the contract that validates arguments before execution; an adopted tool carries the foreign `inputSchema` as `parameters` and validates nothing | `G5c` § 1; `G5` § 8 |
4919
- | `ModelContextTool.title` | `ToolDefinition.title` | implement | `G5c` § 1 |
4920
- | `ToolAnnotations` hints | `ToolAnnotations { pure, untrusted, consequential }`, projected by `toolAnnotationsToWebMCP` | implement — `pure` to `readOnlyHint`, `untrusted` to `untrustedContentHint`, `consequential` to `consequentialHint`; the MCP wire carries `pure` to `readOnlyHint` and `consequential` to `destructiveHint` and drops `untrusted` | `G5c` § 1, § 4 |
4921
- | Declarative form attributes | none | exclude — `index.bs` marks that section "entirely a TODO", the submission path is a fetch-tool paraphrase, and markup is application policy | `G5c` § 3 |
4922
- | Consent and user activation | none; `ToolAnnotations.consequential` is the datum a consent layer reads | exclude — no source states an activation requirement, and consent belongs to the user agent | `G5` § 5 |
4923
- | Origin scoping (`Permissions-Policy: tools`, `allow="tools"`) | `createScopeServer`'s `accept` gate, and `publish`/`adopt`'s `origins` | implement the library half; the header is the application's | `G5c` § 5 |
4924
- | Cross-document handshake from the client side | none | exclude — separate origin, navigation, and lifecycle work with its own design round | `G5c` § 5 |
4925
- | Result shape (`Promise<DOMString>` in the IDL against `{ content: [...] }` in the README sample) | the executed tool's value, resolved unchanged | exclude the contradiction, retain ours — the primary source disagrees with itself, and the bridge projects to MCP content blocks in neither direction | `G5c` § 1, § 4 |
4926
- | Structured refusal | a rejection carrying the failure's message, which is what the specification's own sample catches | exclude — the specification's issue #282 leaves the shape open; do not invent one | `G5` § 8 |
4927
- | Streaming and partial result on abort | `notifications/progress` plus `MCPCallOptions.progress` | retain, exceeds — WebMCP defines none | `G5` § 8 |
4928
- | Resources, prompts, sampling, elicitation, tasks | published by `MCPServer` today | retain — no WebMCP counterpart was found in the sources reached, and whether WebMCP addresses any of them is recorded as unknown rather than as an absence | `G5` § 6 |
4929
- | Server-initiated requests | none | exclude — a modern-protocol non-goal recorded under [Declared non-goals](#declared-non-goals), not a WebMCP gap | `G5` § 6 |
4930
- | Shipping status | `createModelContext` returning `undefined` | exclude as a dependency — the detection is the return value, there is no `supported` flag, and the chromestatus record, read 2026-09-15 and last updated 2026-08-12, reports `Proposed` with `"flag": false` and `"origintrial": false` | `G5c` § 7 |
4992
+ surrounding research context. The execution-event and debugging rows use the 2026-09-29 draft
4993
+ source (`index.bs`, see the [WebMCP specification draft](https://webmachinelearning.github.io/webmcp/)); the webref IDL snapshot omits those members (see the [webref repository](https://github.com/w3c/webref)). `createModelContext` is where
4994
+ the implemented rows live.
4995
+
4996
+ | WebMCP surface | Ours | Verdict | Source |
4997
+ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
4998
+ | `registerTool(tool, { exposedTo, signal })` | `ModelContextInterface.publish(tools, { origins })` | implement — `origins` becomes `exposedTo`, and the retained controller's abort is the unregistration path | `G5c` § 1, § 2 |
4999
+ | `getTools({ fromOrigins })` | `ModelContextInterface.adopt({ origins })` | implement — `origins` becomes `fromOrigins`, and each `RegisteredTool` comes back as a `ToolInterface` | `G5c` § 1 |
5000
+ | `executeTool(tool, input, { signal })` | an adopted tool's `execute(args, context)`, and `ToolManagerInterface.execute` on the other side | implement — the caller's `ToolContext.signal` becomes WebMCP's `signal` in both directions | `G5c` § 1 |
5001
+ | `toolchange` event | `ModelContextEventMap.change` | implement — the registry names no changed tool, so ours is a bare signal and a listener re-reads `adopt` | `G5c` § 2 |
5002
+ | `toolactivated` event | `ModelContextEventMap.activate` | implement — republishes the tool name when execution begins | 2026-09-29 draft, `index.bs` |
5003
+ | `toolcancel` event | `ModelContextEventMap.abort` | implement — republishes the tool name; `abort` is the fixed lifecycle vocabulary for cancellation | 2026-09-29 draft, `index.bs` |
5004
+ | `ToolAnnotations.debugging` | `adopt({ debugging })` | implement — excluded by default; the `debugging` option includes the tool | 2026-09-29 draft, `index.bs` |
5005
+ | Registry change events on our own tool registry | `ToolManagerEventMap` (`add`, `remove`, `clear`), followed by `publish` | implement — `@orkestrel/tool` publishes registry events, so `publish` subscribes to the manager's `emitter` and the document registry tracks the tool registry with no second call | `G5c` § 2 |
5006
+ | `inputSchema` (JSON Schema) | `ToolDefinition.parameters`, derived from `ToolOptions.contract` | implement, exceeds in the publish direction — a published tool carries the contract that validates arguments before execution; an adopted tool carries the foreign `inputSchema` as `parameters` and validates nothing | `G5c` § 1; `G5` § 8 |
5007
+ | `ModelContextTool.title` | `ToolDefinition.title` | implement | `G5c` § 1 |
5008
+ | `ToolAnnotations` hints | `ToolAnnotations { pure, untrusted, consequential }`, projected by `toolAnnotationsToWebMCP` | implement — `pure` to `readOnlyHint`, `untrusted` to `untrustedContentHint`, `consequential` to `consequentialHint`; the MCP wire carries `pure` to `readOnlyHint` and `consequential` to `destructiveHint` and drops `untrusted` | `G5c` § 1, § 4 |
5009
+ | Declarative form attributes | none | exclude — `index.bs` marks that section "entirely a TODO", the submission path is a fetch-tool paraphrase, and markup is application policy | `G5c` § 3 |
5010
+ | Consent and user activation | none; `ToolAnnotations.consequential` is the datum a consent layer reads | exclude — no source states an activation requirement, and consent belongs to the user agent | `G5` § 5 |
5011
+ | Origin scoping (`Permissions-Policy: tools`, `allow="tools"`) | `createScopeServer`'s `accept` gate, and `publish`/`adopt`'s `origins` | implement the library half; the header is the application's | `G5c` § 5 |
5012
+ | Cross-document handshake from the client side | none | exclude — separate origin, navigation, and lifecycle work with its own design round | `G5c` § 5 |
5013
+ | Result shape (`Promise<DOMString>` in the IDL against `{ content: [...] }` in the README sample) | the executed tool's value, resolved unchanged | exclude the contradiction, retain ours — the primary source disagrees with itself, and the bridge projects to MCP content blocks in neither direction | `G5c` § 1, § 4 |
5014
+ | Structured refusal | a rejection carrying the failure's message, which is what the specification's own sample catches | exclude — the specification's issue #282 leaves the shape open; do not invent one | `G5` § 8 |
5015
+ | Streaming and partial result on abort | `notifications/progress` plus `MCPCallOptions.progress` | retain, exceeds — WebMCP defines none | `G5` § 8 |
5016
+ | Resources, prompts, sampling, elicitation, tasks | published by `MCPServer` today | retain — no WebMCP counterpart was found in the sources reached, and whether WebMCP addresses any of them is recorded as unknown rather than as an absence | `G5` § 6 |
5017
+ | Server-initiated requests | none | exclude — a modern-protocol non-goal recorded under [Declared non-goals](#declared-non-goals), not a WebMCP gap | `G5` § 6 |
5018
+ | Shipping status | `createModelContext` returning `undefined` | exclude as a dependency — the detection is the return value, there is no `supported` flag, and the chromestatus record, read 2026-09-15 and last updated 2026-08-12, reports `Proposed` with `"flag": false` and `"origintrial": false` | `G5c` § 7 |
4931
5019
 
4932
5020
  ## Declared conformance gaps
4933
5021
 
@@ -4938,7 +5026,8 @@ A reproducible run is `npm run test:conformance`: it starts the real Streamable
4938
5026
  server from this package's source and runs `@modelcontextprotocol/conformance` — the
4939
5027
  release `package.json` pins as a development dependency — against specification revision
4940
5028
  `2026-07-28`. That is a genuine foreign MCP client driving this surface end to end, and the
4941
- recorded server-mode result is **110 passed / 0 failed**, the `dns-rebinding-protection`
5029
+ recorded server-mode result is **147 passed / 0 failed** at runner `0.2.0-alpha.11`, from
5030
+ the run on 2026-09-29 against the built `dist/`, the `dns-rebinding-protection`
4942
5031
  security regression guard (2 passed) included. `tests/conformance.test.ts` records that
4943
5032
  result scenario by scenario, so a scenario that stops running reddens instead of vanishing
4944
5033
  into a total. `http-custom-header-server-validation` is green at 9 passed: SEP-2243 wants a
@@ -5033,7 +5122,7 @@ scope:
5033
5122
 
5034
5123
  **The browser face's honest proof is a real host, not a foreign client.** There is no cheap
5035
5124
  foreign browser MCP client to point at it, and inventing one would be a worse instrument
5036
- than naming the limit — a fixture we wrote agreeing with code we wrote is not independent
5125
+ than naming the limit — a fixture this package's authors wrote agreeing with code they wrote is not independent
5037
5126
  evidence. So the browser claims are proven by Playwright driving real Chromium: a real
5038
5127
  `WebSocket`, a real `fetch`, and a real `MessageChannel`, against a real Node server running
5039
5128
  outside the page's module graph. That is a real **host** exercising the real platform APIs,
@@ -5145,19 +5234,6 @@ listing keeps projecting. Arrival order cannot merge two listings into one table
5145
5234
  another cursorless `tools/list` supersedes before its answer arrives is still delivered to
5146
5235
  the caller, exclusions and all, and caches nothing.
5147
5236
 
5148
- **The stdio client shuts a child down signal-first, not stdin-first — a declared `SHOULD`
5149
- departure owned by another package.** The stdio page says a client `SHOULD` close the child's
5150
- `stdin`, wait for it to exit, and terminate it only if it does not. `StdioClientTransport.close`
5151
- runs `@orkestrel/process`'s bounded teardown, whose ladder is the other way round: the
5152
- supervisor signals the child (`SIGTERM`, then `SIGKILL` after the grace window; on Windows a
5153
- `taskkill /F /T` over the tree), and destroys `stdin` after that. **What it costs:** a child
5154
- that would have exited cleanly on EOF is signalled instead, so its own shutdown work runs
5155
- against a deadline and, on Windows, does not run at all — a `SIGTERM` handler never fires there,
5156
- and the diagnostics it would have written never exist. **Closer:** `@orkestrel/process`. The
5157
- ladder belongs to the supervisor that owns the child, not to a transport reaching around it, so
5158
- this package adopts a cooperative stop as soon as `Process` offers one; the improvement is
5159
- recorded against that package.
5160
-
5161
5237
  **Tool-invocation rate limiting — not satisfied, and no unit will close it.**
5162
5238
  2025-11-25's `server/tools` § Security Considerations binds a server to validate tool
5163
5239
  inputs, implement access controls, **rate limit tool invocations**, and sanitize tool
@@ -5288,7 +5364,9 @@ assuming, and every bridge scenario runs against `tests/fixtures/modelContext.ts
5288
5364
  implementation of that WebIDL, member for member and nothing beyond it. **What that proves:**
5289
5365
  the translation each way — the projected annotations, the exposure and origin options, the
5290
5366
  signal carried into a published tool and out to an adopted one, the `toolchange` republication,
5291
- and the registration ownership `destroy` releases. **What it does not prove:** that a user
5367
+ `toolactivated` republished as `activate`, `toolcancel` republished as `abort`, the default
5368
+ exclusion of `annotations.debugging: true` tools unless `adopt({ debugging: true })` requests
5369
+ them, and the registration ownership and event listeners `destroy` releases. **What it does not prove:** that a user
5292
5370
  agent's own registry accepts these registrations, or that an agent driving one reaches this
5293
5371
  page's tools. **What it costs:** nothing a consumer can reach today, because
5294
5372
  `createModelContext` answers `undefined` on every shipping browser, so feature absence stays
@@ -5313,7 +5391,8 @@ omission, and a consumer meets each of them at install time rather than in a bui
5313
5391
 
5314
5392
  **IDE integration is not claimed.** A real foreign protocol client drives the Streamable
5315
5393
  HTTP surface end to end — `@modelcontextprotocol/conformance` against revision
5316
- `2026-07-28`, recorded at 110 passed / 0 failed — and that is a claim about the
5394
+ `2026-07-28`, recorded at 147 passed / 0 failed by runner `0.2.0-alpha.11` in the run on
5395
+ 2026-09-29 against the built `dist/` — and that is a claim about the
5317
5396
  wire. No IDE, editor, or agent host has driven this server. The rule is this repository's
5318
5397
  own: a claim about an external client stays unproven until one representative real client
5319
5398
  of that class drives it end to end, and no client of the IDE class has. **What it costs:**
@@ -5589,7 +5668,7 @@ the exact statement:
5589
5668
  The lookup follows `nextCursor` through at most `MCP_LOOKUP_PAGES` pages, so a
5590
5669
  replacement `tools/list` that pages the named tool further in than that recognizes
5591
5670
  none either; each page dispatched fires the `request` event under the reserved id `0`.
5592
- Headerless `initialize` is accepted; a live-session legacy request uses its
5671
+ Headerless `initialize` and id-bearing legacy `ping` are accepted; a live-session legacy request uses its
5593
5672
  pinned negotiated revision; every other headerless request is **400** +
5594
5673
  `-32020`. A request without `Origin` is allowed; a canonical `localhost`, `[::1]`,
5595
5674
  or `127.0.0.0/8` literal origin is allowed by default; every other present
@@ -5849,7 +5928,11 @@ the exact statement:
5849
5928
  parses to an `initialize` request (`isInitializeRequest`) mints a fresh
5850
5929
  `MCPSession` (`crypto.randomUUID()`, the `session` knobs), pins the negotiated legacy
5851
5930
  revision, and sets
5852
- `context.state.session`; neither → `rejectUnknownSession()` (`404`). It
5931
+ `context.state.session`. A legacy `ping` request with no session header passes through
5932
+ without a session state write, protocol-header supply, store write-back, or response stamp.
5933
+ Other unresolved requests return `rejectUnknownSession()` (`404`). The candidate is stored
5934
+ and advertised only after `context.state.initialization` records a successful dispatch
5935
+ result, independently of JSON or SSE framing. It
5853
5936
  then forwards a fresh `Request` carrying the buffered text
5854
5937
  (`next(forwarded)`) — never the already-consumed original — so the route
5855
5938
  re-reads the same body, retains front-middleware state for caller extraction,
@@ -5908,7 +5991,7 @@ the exact statement:
5908
5991
  later `data` listener does not resume that stream; the caller must call
5909
5992
  `resume()` before the listener receives data.
5910
5993
  `createStdioClientTransport(options)` builds one supervised
5911
- `@orkestrel/process` `Process` over `options.command` and `options.args`.
5994
+ `@orkestrel/process` `Supervisor` over `options.command` and `options.args`.
5912
5995
  That supervisor spawns with `stdio: ['pipe', 'pipe', 'pipe']`, so the
5913
5996
  child's `stderr` is piped and retained as a bounded tail rather than
5914
5997
  inherited by the parent. A provided `env` merges over `process.env` rather
@@ -5933,7 +6016,7 @@ the exact statement:
5933
6016
  again, so a detached descendant holding the inherited `stderr` can write
5934
6017
  after `close()` resolves and those bytes reach no reading this transport
5935
6018
  reports. What the frozen tail holds is what the supervisor had received by
5936
- that moment, rather than the child's complete output: Windows ends the tree
6019
+ that moment, rather than the child's complete output: after the input grace, Windows ends the tree
5937
6020
  with `taskkill /F /T`, which nothing in the child can intercept, so a
5938
6021
  `SIGTERM` handler never runs there and the bytes it would have written never
5939
6022
  exist. A child that exits on its own closes its `stderr` first, so that tail
@@ -5978,18 +6061,17 @@ the exact statement:
5978
6061
  deadline for a peer that did not answer; an explicit `0` removes the bound
5979
6062
  and leaves such a write pending on the channel until teardown settles it as
5980
6063
  the same rejection.
5981
- The child's `stdout` is drained through the supervisor's
5982
- `readline`-framed `lines` iterable and every complete line is decoded onto
6064
+ The child's `stdout` is drained through Node's `readline` and every complete line is decoded onto
5983
6065
  `message` through the shared `dispatchLines` helper (a malformed line emits
5984
6066
  `error`); the child's exit bridges to the transport's `close`. `close()`
5985
- runs the supervisor's bounded termination and teardown, which ends that
5986
- `lines` stream at the child's terminal moment rather than throwing at the
5987
- pump, and fires `close` once. A line already framed behind the one being
6067
+ ends stdin and waits up to `MCP_STDIO_GRACE` for native exit before the supervisor
6068
+ terminates a child that remains alive. The input flush shares that bound. Teardown closes
6069
+ the reader at the child's terminal moment and fires `close` once. A line already framed behind the one being
5988
6070
  delivered is dropped rather than emitted after the teardown began, and the
5989
6071
  wait for a descendant-held stdout pipe is capped by the supervisor's `drain`
5990
6072
  bound; a `close()` issued while that teardown runs joins it and resolves only
5991
6073
  after `close` has fired. The
5992
- termination is the host's — a POSIX host signals the child's own process
6074
+ escalation is the host's — a POSIX host signals the child's own process
5993
6075
  group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
5994
6076
  route, while Windows ends the tree with `taskkill /F /T`.
5995
6077
  The stdio transports'