@orkestrel/scaffold 0.0.69 → 0.0.71
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.
- package/dist/bin/main.js +8 -8
- package/dist/bin/main.js.map +1 -1
- package/dist/host/claude/agents/orkestrel.md +7 -7
- package/dist/host/guides/mcp.md +615 -63
- package/dist/host/guides/ollama.md +9 -8
- package/dist/host/guides/scaffold.md +19 -8
- package/dist/host/manifest.json +5 -5
- package/dist/src/core/index.cjs +11 -11
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +12 -12
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +4 -4
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +5 -5
- package/dist/src/server/index.js.map +1 -1
- package/package.json +8 -8
package/dist/host/guides/mcp.md
CHANGED
|
@@ -911,22 +911,38 @@ resemblance to one.
|
|
|
911
911
|
|
|
912
912
|
### Configure modern subscriptions
|
|
913
913
|
|
|
914
|
-
`
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
914
|
+
The server supplies `toolsListChanged` from the tool registry's `add`, `remove`, and `clear`
|
|
915
|
+
events. It registers the listeners before acknowledging the stream and releases them when
|
|
916
|
+
`options.signal` aborts. Registry destruction delivers its final `clear`; the stream then
|
|
917
|
+
stays open until its signal aborts. An already-destroyed registry registers no listeners; the
|
|
918
|
+
server acknowledges the subscription, produces no tools frames, and waits for its signal. A
|
|
919
|
+
stream read between changes receives one frame per change; changes that arrive together, or
|
|
920
|
+
while the previous frame is unread, coalesce into one.
|
|
921
|
+
|
|
922
|
+
`subscription.notifications` declares the consumer's supported families through
|
|
923
|
+
`MCPConsumerFilter`, whose optional `toolsListChanged` member accepts only `false`;
|
|
924
|
+
`subscription.producer` opens the event-driven source for the intersected filter, beside the
|
|
925
|
+
built-in tools producer. The consumer producer advances on the stream's demand. A producer
|
|
926
|
+
failure terminates the stream after the notifications it already produced. A consumer filter
|
|
927
|
+
that is malformed, and one claiming `toolsListChanged: true`, are both invalid: construction
|
|
928
|
+
throws `MCPError` with `JSONRPC_INVALID_PARAMS` (`-32602`). Consumer-produced tools
|
|
929
|
+
notifications are dropped, so the tools family has only the registry as its source. The
|
|
930
|
+
built-in owns wire acknowledgement, filtering, id stamping, and graceful closure. A producer
|
|
931
|
+
only yields project notifications and ends its iterable when the source closes; while idle it
|
|
932
|
+
parks on its own events and may observe the supplied abort signal. Releasing the producer
|
|
933
|
+
cancels its source with that signal's abort reason, so a `ReadableStream` source sees its
|
|
934
|
+
pending writes reject with the reason the request was aborted with.
|
|
935
|
+
|
|
936
|
+
**Every produced notification is owned before it is judged.** The built-in snapshots each one
|
|
937
|
+
into bounded exact JSON before it matches the filter or stamps the id, so the values that
|
|
938
|
+
admitted a notification are the values that reach the wire — a producer answering differently
|
|
939
|
+
on a second read cannot have one URI pass the filter and another ride out. A notification
|
|
940
|
+
that is not bounded exact JSON is dropped and the stream continues; a producer that throws
|
|
941
|
+
ends the subscription after queued notifications with one detail-free `-32603` terminal, its
|
|
942
|
+
caught value reported on the server's `error` event. Ending the consumer source normally
|
|
943
|
+
closes with the complete result only when the honoured filter omits `toolsListChanged`. A
|
|
944
|
+
stream honouring the tools family stays open until failure or signal abort. An abort closes
|
|
945
|
+
with no terminal at all, because a cancelled request is not an answered one.
|
|
930
946
|
|
|
931
947
|
#### Configure the subscription producer
|
|
932
948
|
|
|
@@ -946,6 +962,7 @@ import {
|
|
|
946
962
|
buildSubscriptionFilter,
|
|
947
963
|
buildSubscriptionResult,
|
|
948
964
|
createMCPServer,
|
|
965
|
+
isMCPConsumerFilter,
|
|
949
966
|
isMCPSubscriptionFilter,
|
|
950
967
|
matchesSubscriptionNotification,
|
|
951
968
|
stampSubscriptionNotification,
|
|
@@ -953,11 +970,13 @@ import {
|
|
|
953
970
|
import { createToolManager } from '@orkestrel/tool'
|
|
954
971
|
|
|
955
972
|
const identity = { name: 'docs', version: '1.0.0' }
|
|
956
|
-
const supported = {
|
|
957
|
-
|
|
973
|
+
const supported = { promptsListChanged: true, resourceSubscriptions: ['resource://guide'] }
|
|
974
|
+
isMCPConsumerFilter(supported) // true
|
|
975
|
+
isMCPConsumerFilter({ toolsListChanged: true }) // false — the server owns this family
|
|
976
|
+
const input: unknown = { promptsListChanged: true, resourcesListChanged: true }
|
|
958
977
|
if (!isMCPSubscriptionFilter(input)) throw new Error('invalid filter')
|
|
959
978
|
const honoured = buildSubscriptionFilter(input, supported)
|
|
960
|
-
const event: JSONRPCNotification = { jsonrpc: '2.0', method: 'notifications/
|
|
979
|
+
const event: JSONRPCNotification = { jsonrpc: '2.0', method: 'notifications/prompts/list_changed' }
|
|
961
980
|
matchesSubscriptionNotification(event, honoured) // true
|
|
962
981
|
stampSubscriptionNotification(event, 'listen-1') // every delivery carries the reserved id
|
|
963
982
|
buildSubscriptionAcknowledgement(honoured, 'listen-1') // the first id-carrying message
|
|
@@ -1114,7 +1133,7 @@ await client.connect()
|
|
|
1114
1133
|
|
|
1115
1134
|
const subscription = new AbortController()
|
|
1116
1135
|
const stream = client.listen(
|
|
1117
|
-
{
|
|
1136
|
+
{ promptsListChanged: true, resourceSubscriptions: ['resource://guide'] },
|
|
1118
1137
|
{ signal: subscription.signal, capacity: 16 },
|
|
1119
1138
|
)
|
|
1120
1139
|
|
|
@@ -1146,7 +1165,11 @@ subscription.abort()
|
|
|
1146
1165
|
|
|
1147
1166
|
`MCPServerOptions.execution` is the explicit modern execution port over the live
|
|
1148
1167
|
`ToolManagerInterface`. Its input contains the original `request`, canonical `call`,
|
|
1149
|
-
real `tools` manager, effective `signal`, and an optional `progress` reporter.
|
|
1168
|
+
real `tools` manager, effective `signal`, optional `caller`, and an optional `progress` reporter.
|
|
1169
|
+
The default path calls `tools.execute(call, { signal, caller })`; caller identity is omitted
|
|
1170
|
+
when absent and never placed on the JSON call envelope. A delegating handler calls
|
|
1171
|
+
`tools.execute(context.call, { signal: context.signal, ...(context.caller === undefined ? {} : { caller: context.caller }) })`.
|
|
1172
|
+
A tool handler observes `context.signal` to stop work when its request is cancelled. Returning
|
|
1150
1173
|
a `ToolResult` uses the normal text/structured normalization; returning a validated
|
|
1151
1174
|
`MCPCallResult` preserves exact text, image, audio, resource-link, and embedded-resource
|
|
1152
1175
|
content without guessing from an ordinary domain value.
|
|
@@ -2254,6 +2277,7 @@ A `Shape` cell holds the constant's declared type.
|
|
|
2254
2277
|
| `isMCPContent` | function | Determines whether a value is one exact dated-schema MCP tool content block. |
|
|
2255
2278
|
| `isMCPPaginationParams` | function | Determines whether a value carries the shared optional pagination cursor. |
|
|
2256
2279
|
| `isMCPResource` | function | Determines whether a value is one `resources/list` descriptor. |
|
|
2280
|
+
| `isMCPToolAnnotations` | function | Checks whether a value carries valid consumed MCP 2026-07-28 tool annotation hints. |
|
|
2257
2281
|
| `isMCPResourceTemplate` | function | Determines whether a value is one resource-template descriptor. |
|
|
2258
2282
|
| `isMCPResourceContents` | function | Determines whether a value is structurally discriminated resource contents. |
|
|
2259
2283
|
| `isMCPResourcePage` | function | Determines whether a value is one consumer-owned resource page. |
|
|
@@ -2287,6 +2311,7 @@ A `Shape` cell holds the constant's declared type.
|
|
|
2287
2311
|
| `isInitializeRequest` | function | Determines whether a parsed value is an MCP `initialize` invocation. |
|
|
2288
2312
|
| `isMCPVersion` | function | Determines whether a value is a supported `MCPVersion`. |
|
|
2289
2313
|
| `isMCPSubscriptionFilter` | function | Determines whether a value is an MCP `MCPSubscriptionFilter`. |
|
|
2314
|
+
| `isMCPConsumerFilter` | function | Checks whether a subscription filter leaves the built-in tools family to the server. |
|
|
2290
2315
|
| `isMCPSubscriptionResult` | function | Determines whether a value is a graceful `subscriptions/listen` result. |
|
|
2291
2316
|
| `supportsFormElicitation` | function | Determines whether a client capability record declares form-mode elicitation. |
|
|
2292
2317
|
| `isMCPElicitFieldSchema` | function | Determines whether a value is one restricted primitive form-elicitation schema. |
|
|
@@ -2326,6 +2351,8 @@ A `Shape` cell holds the constant's declared type.
|
|
|
2326
2351
|
| `buildJSONRPCError` | function | Builds a JSON-RPC error `JSONRPCErrorResponse` — the `id` echoed, the failure as an `error` object. |
|
|
2327
2352
|
| `buildMethodOptions` | function | Resolves the caller-facing dispatch options into the options a dispatched method receives. |
|
|
2328
2353
|
| `buildToolDescriptors` | function | Maps a `ToolManagerInterface`'s definitions to MCP `tools/list` descriptors — renaming `parameters` to the wire's `inputSchema`. |
|
|
2354
|
+
| `toolAnnotationsToMCP` | function | Projects domain tool annotations onto MCP wire hints without inventing defaults. |
|
|
2355
|
+
| `mcpAnnotationsToTool` | function | Projects MCP wire hints onto domain tool annotations without inventing defaults. |
|
|
2329
2356
|
| `buildToolCall` | function | Builds the canonical Tool call for one validated MCP `tools/call` request. |
|
|
2330
2357
|
| `buildProgressNotification` | function | Builds one official progress notification for the original request stream. |
|
|
2331
2358
|
| `buildCancelledNotification` | function | Builds one official cancellation notification for a request already sent. |
|
|
@@ -2368,6 +2395,20 @@ A `Shape` cell holds the constant's declared type.
|
|
|
2368
2395
|
| `readEventStream` | function | Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it carried — the client-side inverse of a server's Streamable-HTTP SSE response. |
|
|
2369
2396
|
| `buildResponseError` | function | Builds the error for a non-success HTTP response that carried no JSON-RPC message. |
|
|
2370
2397
|
|
|
2398
|
+
### Project tool annotation hints
|
|
2399
|
+
|
|
2400
|
+
The projection keeps explicit false values and omits hints with no counterpart.
|
|
2401
|
+
|
|
2402
|
+
```ts
|
|
2403
|
+
import { toolAnnotationsToMCP, mcpAnnotationsToTool, isMCPToolAnnotations } from '@orkestrel/mcp'
|
|
2404
|
+
|
|
2405
|
+
toolAnnotationsToMCP({ pure: true, consequential: false, untrusted: true })
|
|
2406
|
+
// { readOnlyHint: true, destructiveHint: false }
|
|
2407
|
+
mcpAnnotationsToTool({ readOnlyHint: false, destructiveHint: true })
|
|
2408
|
+
// { pure: false, consequential: true }
|
|
2409
|
+
isMCPToolAnnotations({ readOnlyHint: false }) // true
|
|
2410
|
+
```
|
|
2411
|
+
|
|
2371
2412
|
### Types
|
|
2372
2413
|
|
|
2373
2414
|
A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an
|
|
@@ -2481,16 +2522,18 @@ An extended interface's name comes before `plus`, with the members it adds after
|
|
|
2481
2522
|
| `MCPProgress` | interface | `{ progress, total?, message? }` | Represents one official request-scoped progress payload. |
|
|
2482
2523
|
| `MCPProgressInterface` | interface | `{} plus report` | Reports request-scoped progress under backpressure — the reporter supplied to an explicit executor. |
|
|
2483
2524
|
| `MCPProgressOwnerInterface` | interface | `MCPProgressInterface plus take, stop` | Represents the owning half of one progress slot — `MCPProgressInterface` plus the consuming and stopping the slot's owner performs. |
|
|
2484
|
-
| `MCPExecutionContext` | interface | `{ request, call, tools, signal, progress? }`
|
|
2525
|
+
| `MCPExecutionContext` | interface | `{ request, call, tools, signal, caller?, progress? }` | Represents the explicit, host-neutral context for one modern tool execution. |
|
|
2485
2526
|
| `MCPExecutionHandler` | type | `(context: MCPExecutionContext,) => ToolResult \| MCPCallResult \| Promise<ToolResult \| MCPCallResult>` | Executes one canonical tool call or returns a fully formed complete MCP result. |
|
|
2486
2527
|
| `MCPListResult` | type | `{ tools, resultType: 'complete', ttlMs, cacheScope: 'public' \| 'private', _meta? }` | Represents the MCP `tools/list` result — tool descriptors plus optional modern result stamps. |
|
|
2487
|
-
| `
|
|
2528
|
+
| `MCPToolAnnotations` | interface | `{ title?, readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }` | Describes tool hints using the MCP 2026-07-28 specification's wire field names. |
|
|
2529
|
+
| `MCPToolDescriptor` | interface | `{ name, title?, description?, inputSchema, annotations? }` | Represents one entry of the MCP `tools/list` result with its display metadata and JSON-Schema input. |
|
|
2488
2530
|
| `MCPHeaderPrimitive` | type | `'boolean' \| 'integer' \| 'string'` | Names the JSON Schema types an `x-mcp-header` annotation may sit on. |
|
|
2489
2531
|
| `MCPHeaderParameter` | interface | `{ name, path, primitive }` | Represents one `x-mcp-header` projection a tool's `inputSchema` declares. |
|
|
2490
2532
|
| `MCPIdentity` | type | `MCPMetaObject & { name, version, title?, description?, websiteUrl?, icons? }` | Represents the complete dated identity of an MCP server or client. |
|
|
2491
2533
|
| `MCPRequestContext` | interface | `{ version, capabilities, identity? }` | Represents the validated per-request context projected from a modern request's reserved `_meta` keys. |
|
|
2492
2534
|
| `MCPDiscoverResult` | type | `{ supportedVersions, capabilities, resultType: 'complete', ttlMs, cacheScope: 'public' \| 'private', instructions?, _meta? }` | Represents the mandatory modern `server/discover` result. |
|
|
2493
|
-
| `MCPSubscriptionFilter` | interface | `{ toolsListChanged?, promptsListChanged?, resourcesListChanged?, resourceSubscriptions?, taskIds? }` | Names the notification families a client may opt in to on a `subscriptions/listen` stream.
|
|
2535
|
+
| `MCPSubscriptionFilter` | interface | `{ toolsListChanged?, promptsListChanged?, resourcesListChanged?, resourceSubscriptions?, taskIds? }` | Names the notification families a client may opt in to on a `subscriptions/listen` stream, including built-in registry changes. |
|
|
2536
|
+
| `MCPConsumerFilter` | interface | `MCPSubscriptionFilter plus { toolsListChanged?: false }` | Declares the consumer-produced notification families, excluding registry-owned tools changes. |
|
|
2494
2537
|
| `MCPSubscriptionResultMetaObject` | type | `MCPResultMetaObject & { 'io.modelcontextprotocol/subscriptionId' }` | Represents the required metadata on a graceful `subscriptions/listen` result. |
|
|
2495
2538
|
| `MCPSubscriptionResult` | type | `{ resultType: 'complete', _meta }` | Represents the terminating result returned when a `subscriptions/listen` stream closes gracefully. |
|
|
2496
2539
|
| `MCPSubscriptionStream` | type | `AsyncGenerator< JSONRPCNotification, MCPSubscriptionResult, unknown >` | Represents a client subscription's owned notifications and graceful terminal result. |
|
|
@@ -2595,8 +2638,8 @@ HTTP header validation have all passed, immediately before `mcp.dispatch`. With
|
|
|
2595
2638
|
extractor, or when it returns `undefined`, `caller` is omitted through the
|
|
2596
2639
|
package's conditional-spread idiom, preserving the former dispatch-options shape
|
|
2597
2640
|
exactly. A present value flows through both modern and legacy `tools/call` onto
|
|
2598
|
-
`
|
|
2599
|
-
|
|
2641
|
+
the execution context's `caller` member; the tool manager supplies that context to the
|
|
2642
|
+
real tool body's `context` parameter.
|
|
2600
2643
|
|
|
2601
2644
|
This remains an asserted seam, never protocol authentication. A session id names
|
|
2602
2645
|
an HTTP transport session, not a caller, and the session middleware preserves
|
|
@@ -3162,9 +3205,45 @@ parameter defaults to `globalThis`, so a worker boots with
|
|
|
3162
3205
|
`createScopeServer({ tools })` alone and a test drives the same wiring by passing a scope
|
|
3163
3206
|
double instead of a real worker.
|
|
3164
3207
|
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
and
|
|
3208
|
+
`createPageServer` is that bootstrap's page twin, and the in-page pair as one call. It owns a
|
|
3209
|
+
native `MessageChannel`, binds an `MCPServer` to one half and an `MCPClientInterface` to the
|
|
3210
|
+
other, and hands the client back on `PageServerInterface`. Assembling the same pair by hand
|
|
3211
|
+
means writing the channel, two transports, `bindServer`, `createDuplexClientTransport`,
|
|
3212
|
+
`createMCPClient`, and `bindClient` in an order `MessagePortTransport`'s own doc warns about —
|
|
3213
|
+
a port starts dispatching at construction, so an `await` interleaved between a transport and
|
|
3214
|
+
its binder drops whatever arrived in the gap. The factory never suspends between the two. The
|
|
3215
|
+
returned client is bound and not connected, because connection is a protocol round trip and a
|
|
3216
|
+
factory that returned a promise could not return the terminal beside it: call
|
|
3217
|
+
`await page.client.connect()` yourself. `stop` closes the client's port first, so the client
|
|
3218
|
+
observes the close and reports `connected` as `false`, then unbinds both sides and closes the
|
|
3219
|
+
server's port; it is idempotent, and it is the verb `createScopeServer`'s handle already
|
|
3220
|
+
publishes for the same action.
|
|
3221
|
+
|
|
3222
|
+
`createModelContext` is the WebMCP bridge: it publishes a `ToolManagerInterface`'s tools to the
|
|
3223
|
+
document's `document.modelContext` registry and reads that registry back as
|
|
3224
|
+
`@orkestrel/tool` tools. Feature detection is the return value — `undefined` means the document
|
|
3225
|
+
exposes no registry — so there is no `supported` flag to read and no polyfill behind the
|
|
3226
|
+
factory. `publish` registers each advertised tool through `registerTool` with the projected
|
|
3227
|
+
annotations, retains the `AbortController` whose abort is WebMCP's own unregistration path, and
|
|
3228
|
+
subscribes to the manager's own `emitter` so a later `add`, `remove`, or `clear` reaches the
|
|
3229
|
+
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
|
|
3231
|
+
`ToolContext.signal`; `destroy` aborts the registrations this handle made, releases that
|
|
3232
|
+
subscription, and leaves a name it never registered alone. WebMCP keys a registration by tool
|
|
3233
|
+
name per document, so a release takes whatever now stands under that name. See
|
|
3234
|
+
[WebMCP parity](#webmcp-parity) for the surface comparison, and
|
|
3235
|
+
[Declared conformance gaps](#declared-conformance-gaps) for what a browser that ships no
|
|
3236
|
+
registry leaves unproven.
|
|
3237
|
+
|
|
3238
|
+
**The transports are host-neutral; the WebMCP bridge is not, and it says so at runtime.** This
|
|
3239
|
+
face type-checks against `lib: ["ESNext", "DOM", "DOM.Iterable"]`
|
|
3240
|
+
(`configs/src/tsconfig.browser.json`), because `createModelContext` takes a `Document` and
|
|
3241
|
+
`WebMCPRegisteredTool` carries a `Window`. Nothing else here touches a DOM-only global:
|
|
3242
|
+
`WebSocket`, `fetch`, `MessagePort`, and `MessageChannel` exist in a page, a Web Worker, and a
|
|
3243
|
+
Service Worker alike, so every transport, `createScopeServer`, and `createPageServer` run
|
|
3244
|
+
identically in all three. `createModelContext` reads `globalThis.document`, which is `undefined`
|
|
3245
|
+
in a worker, and answers `undefined` there — the same answer it gives a page whose browser
|
|
3246
|
+
ships no registry.
|
|
3168
3247
|
|
|
3169
3248
|
```ts
|
|
3170
3249
|
import { createMCPClient } from '@orkestrel/mcp'
|
|
@@ -3194,6 +3273,8 @@ const tools = await http.tools()
|
|
|
3194
3273
|
| `createScopeServer` | function | Creates an `MCPServer` hosted inside a worker scope and wires that scope's message events to it — the browser face's bootstrap, and the twin of the Node face's `createStdioServer`. |
|
|
3195
3274
|
| `createScopeTransport` | function | Adapts a hostable `ScopeInterface` (`self` in a dedicated Web Worker, or any structurally matching double) into a `ScopeTransportInterface` — the implicit, portless message channel `createScopeServer` binds for the dedicated-worker shape. |
|
|
3196
3275
|
| `createScopeMessageListener` | function | Builds `createScopeServer`'s `message`-event listener — the unified dispatcher that routes every inbound event on a hostable scope, portless or port-bearing, to the right binding. |
|
|
3276
|
+
| `createPageServer` | function | Creates an `MCPServer` hosted inside the calling page and hands back the client bound to it — the page twin of `createScopeServer`, and the in-page MCP pair as one call. |
|
|
3277
|
+
| `createModelContext` | function | Creates the bridge between a tool registry and a document's WebMCP registry, or reports that the document exposes none. |
|
|
3197
3278
|
|
|
3198
3279
|
#### Classes
|
|
3199
3280
|
|
|
@@ -3201,6 +3282,7 @@ const tools = await http.tools()
|
|
|
3201
3282
|
| -------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3202
3283
|
| `WebSocketClientTransport` | class | Drives a remote MCP server over the native `WebSocket` global from the browser face, as a client `MCPMessageTransportInterface`. This class is the browser sibling of the Node face's `WebSocketClientTransport`. |
|
|
3203
3284
|
| `MessagePortTransport` | class | Carries the Model Context Protocol over a native `MessagePort` from the browser face — a `MCPTransportInterface`, the genuinely new capability this face adds: MCP over `postMessage`. |
|
|
3285
|
+
| `ModelContext` | class | Bridges a `ToolManagerInterface` and a document's WebMCP tool registry — the `ModelContextInterface` `createModelContext` returns. |
|
|
3204
3286
|
|
|
3205
3287
|
#### Constants
|
|
3206
3288
|
|
|
@@ -3210,11 +3292,30 @@ A `Shape` cell holds the constant's declared type.
|
|
|
3210
3292
|
| ---------------------------- | ----- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3211
3293
|
| `DEFAULT_MCP_SERVER_NAME` | const | `'@orkestrel/mcp'` | Supplies the default server name `createScopeServer` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. |
|
|
3212
3294
|
| `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
|
+
| `WEBMCP_CHANGE_EVENT` | const | `'toolchange'` | Names the WebMCP registry event the bridge republishes as its own `change`. |
|
|
3213
3296
|
|
|
3214
3297
|
#### Helpers
|
|
3215
3298
|
|
|
3216
|
-
|
|
3217
|
-
|
|
3299
|
+
This table lists the WebMCP projections, the batches `publish` and a followed change project
|
|
3300
|
+
through, and the registry guards. Each projection is the WebMCP direction of the translation
|
|
3301
|
+
`toolAnnotationsToMCP` and `mcpAnnotationsToTool` perform for the MCP wire; see
|
|
3302
|
+
[Core § Helpers](#helpers) for those.
|
|
3303
|
+
|
|
3304
|
+
| API | Kind | Summary |
|
|
3305
|
+
| -------------------------- | -------- | --------------------------------------------------------------------------------------------- |
|
|
3306
|
+
| `toolAnnotationsToWebMCP` | function | Projects domain tool annotations onto WebMCP registry hints without inventing defaults. |
|
|
3307
|
+
| `webMCPAnnotationsToTool` | function | Projects WebMCP registry hints onto domain tool annotations without inventing defaults. |
|
|
3308
|
+
| `toolToWebMCP` | function | Projects one advertised tool definition onto the WebMCP descriptor a registration carries. |
|
|
3309
|
+
| `describeWebMCPTool` | function | Projects the descriptor a registry advertises for one tool name, or reports that it has none. |
|
|
3310
|
+
| `webMCPToTool` | function | Projects one registered WebMCP tool onto the tool definition an adopted tool advertises. |
|
|
3311
|
+
| `matchesDescriptor` | function | Determines whether two WebMCP descriptors advertise the same tool to the registry. |
|
|
3312
|
+
| `buildWebMCPProjections` | function | Builds the WebMCP projection of every tool a registry advertises, or refuses the batch. |
|
|
3313
|
+
| `collectWebMCPProjections` | function | Collects the WebMCP projection of every tool a registry advertises that WebMCP can carry. |
|
|
3314
|
+
| `isWebMCPRegistry` | function | Determines whether an unknown value is a WebMCP tool registry. |
|
|
3315
|
+
| `isWebMCPDocument` | function | Determines whether an unknown value is a document exposing the WebMCP tool registry. |
|
|
3316
|
+
|
|
3317
|
+
The SSE decoders `decodeEvent` and `readEventStream` are host-independent and ship from
|
|
3318
|
+
`@orkestrel/mcp`; see [Core § Helpers](#helpers).
|
|
3218
3319
|
|
|
3219
3320
|
#### Types
|
|
3220
3321
|
|
|
@@ -3223,14 +3324,33 @@ optional member and `plus` introducing its call-signature members, and a type al
|
|
|
3223
3324
|
literal with a union's arms escaped as `\|`.
|
|
3224
3325
|
An extended interface's name comes before `plus`, with the members it adds after.
|
|
3225
3326
|
|
|
3226
|
-
| Type | Kind | Shape
|
|
3227
|
-
| --------------------------------- | --------- |
|
|
3228
|
-
| `WebSocketClientTransportOptions` | interface | `{ url, protocols? }`
|
|
3229
|
-
| `MessagePortTransportOptions` | interface | `{ port }`
|
|
3230
|
-
| `ScopeInterface` | interface | `{} plus postMessage, addEventListener, removeEventListener`
|
|
3231
|
-
| `ScopeTransportInterface` | interface | `MCPTransportInterface plus deliver`
|
|
3232
|
-
| `ScopeServerInterface` | interface | `{} plus stop`
|
|
3233
|
-
| `ScopeServerOptions` | interface | `{ tools, name?, version? } plus accept?`
|
|
3327
|
+
| Type | Kind | Shape | Summary |
|
|
3328
|
+
| --------------------------------- | --------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3329
|
+
| `WebSocketClientTransportOptions` | interface | `{ url, protocols? }` | Options for `createWebSocketClientTransport` (browser face) — the remote MCP WebSocket endpoint and any negotiated subprotocols. |
|
|
3330
|
+
| `MessagePortTransportOptions` | interface | `{ port }` | Options for `createMessagePortTransport` — the native `MessagePort` a `MessagePortTransport` sends and listens on. |
|
|
3331
|
+
| `ScopeInterface` | interface | `{} plus postMessage, addEventListener, removeEventListener` | Describes the structural shape `createScopeServer` needs from a hostable scope — `self` in a dedicated Web Worker or a Service Worker (or any double matching this shape). |
|
|
3332
|
+
| `ScopeTransportInterface` | interface | `MCPTransportInterface plus deliver` | Adapts a message-event-bearing scope (`self` in a dedicated Web Worker, or any object shaped the same way) as a duplex `MCPTransportInterface` — the internal carrier `createScopeServer` binds to route the implicit (portless) message channel, plus the `deliver` entry point the scope's own `message` listener pushes an inbound string through (the scope itself never registers `listen`'s handler for the caller — the scope server's dispatcher does, through this `deliver`). |
|
|
3333
|
+
| `ScopeServerInterface` | interface | `{} plus stop` | Represents one MCP server hosted inside a worker scope — what `createScopeServer` returns. |
|
|
3334
|
+
| `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
|
+
| `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
|
+
| `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. |
|
|
3338
|
+
| `WebMCPDescriptor` | interface | `{ name, title?, description, inputSchema?, annotations? }` | Describes the members a WebMCP tool carries into the registry and back out of it. |
|
|
3339
|
+
| `WebMCPHandlerOptions` | interface | `{ signal }` | Carries the execution signal the WebMCP registry hands a registered tool's callback. |
|
|
3340
|
+
| `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. |
|
|
3341
|
+
| `WebMCPTool` | interface | `WebMCPDescriptor plus execute` | Describes one tool as handed to the WebMCP registry for registration. |
|
|
3342
|
+
| `WebMCPRegisteredTool` | interface | `WebMCPDescriptor plus window, origin` | Describes one tool as the WebMCP registry reports it back. |
|
|
3343
|
+
| `WebMCPRegisterOptions` | interface | `{ exposedTo?, signal? }` | Options for the WebMCP registry's `registerTool` — the exposure list and the unregistration signal. |
|
|
3344
|
+
| `WebMCPToolsOptions` | interface | `{ fromOrigins? }` | Options for the WebMCP registry's `getTools` — the origins whose tools are read. |
|
|
3345
|
+
| `WebMCPExecuteOptions` | interface | `{ signal? }` | Options for the WebMCP registry's `executeTool` — the caller's cancellation signal. |
|
|
3346
|
+
| `WebMCPRegistryInterface` | interface | `{} plus registerTool, getTools, executeTool, addEventListener, removeEventListener` | Represents the WebMCP tool registry a document exposes as `document.modelContext`. |
|
|
3347
|
+
| `WebMCPDocument` | interface | `{ modelContext }` | Describes a document that exposes the WebMCP tool registry. |
|
|
3348
|
+
| `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. |
|
|
3350
|
+
| `ModelContextOptions` | interface | `{ document?, on?, error? }` | Options for `createModelContext` — the document to bridge and the emitter's initial wiring. |
|
|
3351
|
+
| `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. |
|
|
3353
|
+
| `ModelContextInterface` | interface | `{ emitter } plus publish, adopt, destroy` | Represents the bridge between a `ToolManagerInterface` and a document's WebMCP registry — what `createModelContext` returns. |
|
|
3234
3354
|
|
|
3235
3355
|
_This face declares no `HTTPClientTransportOptions`. It is host-independent and ships from
|
|
3236
3356
|
`@orkestrel/mcp`; see [Core § Types](#types)._
|
|
@@ -3336,7 +3456,8 @@ const reply = await server.handle(
|
|
|
3336
3456
|
{ signal: controller.signal, caller: authenticatedPrincipal },
|
|
3337
3457
|
)
|
|
3338
3458
|
// reply → {"jsonrpc":"2.0","id":2,"result":{"supportedVersions":["2026-07-28"],
|
|
3339
|
-
// "capabilities":{"tools":{}},"resultType":"complete","ttlMs":60000,
|
|
3459
|
+
// "capabilities":{"tools":{"listChanged":true}},"resultType":"complete","ttlMs":60000,
|
|
3460
|
+
// "cacheScope":"private",
|
|
3340
3461
|
// "_meta":{"io.modelcontextprotocol/serverInfo":{"name":"docs","version":"1.0.0"}}}}
|
|
3341
3462
|
```
|
|
3342
3463
|
|
|
@@ -3516,6 +3637,33 @@ The completion port, configured independently of the `resources` and `prompts` p
|
|
|
3516
3637
|
| ---------- | ------------------------------------------- | -------------------------------------------------------- |
|
|
3517
3638
|
| `complete` | `MCPCompletion \| undefined` (or a promise) | Completes one argument against its host-owned reference. |
|
|
3518
3639
|
|
|
3640
|
+
#### `MCPContinuationInterface`
|
|
3641
|
+
|
|
3642
|
+
The integrity and storage port `MCPInputOptions.continuation` requires, implemented by
|
|
3643
|
+
`createMCPContinuation` (`@orkestrel/mcp/server`) and by any consumer port that can protect an
|
|
3644
|
+
opaque string. Core supplies no signer of its own, so the integrity of every binding inside a
|
|
3645
|
+
continuation carrier — principal, expiry, original id, revision, method, tool name, argument
|
|
3646
|
+
digest, and the issued round — rests on whatever port is passed here.
|
|
3647
|
+
|
|
3648
|
+
| Method | Returns | Summary |
|
|
3649
|
+
| ------ | ------------------------------ | ------------------------------------------------------------------------- |
|
|
3650
|
+
| `seal` | `Promise<string>` | Protects a canonical state string and returns the opaque client carrier. |
|
|
3651
|
+
| `open` | `Promise<string \| undefined>` | Recovers a protected canonical state string, or `undefined` when invalid. |
|
|
3652
|
+
|
|
3653
|
+
This example seals a canonical state string and recovers it, then shows a tampered carrier
|
|
3654
|
+
opening as `undefined` — which is what turns a rewritten `requestState` into `-32602` rather
|
|
3655
|
+
than into a trusted claim.
|
|
3656
|
+
|
|
3657
|
+
```ts
|
|
3658
|
+
import { createMCPContinuation } from '@orkestrel/mcp/server'
|
|
3659
|
+
|
|
3660
|
+
const continuation = createMCPContinuation(['current-secret', 'older-secret'])
|
|
3661
|
+
const carrier = await continuation.seal('{"principal":"user-42"}')
|
|
3662
|
+
|
|
3663
|
+
log(await continuation.open(carrier)) // '{"principal":"user-42"}'
|
|
3664
|
+
log(await continuation.open(`${carrier}x`)) // undefined
|
|
3665
|
+
```
|
|
3666
|
+
|
|
3519
3667
|
#### `MCPClientInterface`
|
|
3520
3668
|
|
|
3521
3669
|
The egress mirror: `connect` negotiates the modern revision and stores the selected
|
|
@@ -3527,6 +3675,38 @@ closes the connection it owns. Subscribe to client events through `emitter.on`.
|
|
|
3527
3675
|
The `tasks` data member is the stable Tasks extension's client half — see
|
|
3528
3676
|
[`MCPTaskClientInterface`](#mcptaskclientinterface).
|
|
3529
3677
|
|
|
3678
|
+
`tools()` returns a snapshot. The wire preserves `title`, the advertised `description`,
|
|
3679
|
+
`inputSchema` (wrapped as `parameters`), and the mapped `annotations`. The server maps
|
|
3680
|
+
`pure` to `readOnlyHint` and `consequential` to `destructiveHint`; the client applies the
|
|
3681
|
+
inverse projection. For a schema-less tool, the wire adds `inputSchema: { type: 'object' }`.
|
|
3682
|
+
`untrusted` has no MCP counterpart. Unmapped hints remain absent from
|
|
3683
|
+
the wrapped tool, and omitted hints receive no invented defaults. These hints do not authorize
|
|
3684
|
+
execution. The wire loses `summary` as a separate field and, when a summary was authored,
|
|
3685
|
+
the full description: the registry advertises that summary as `description`.
|
|
3686
|
+
The wrapped tool forwards `context.signal` into `call`, so agent-side abort reaches the
|
|
3687
|
+
remote request. Refresh snapshots explicitly between agent runs; see
|
|
3688
|
+
[Refresh the tools an agent holds](#refresh-the-tools-an-agent-holds).
|
|
3689
|
+
|
|
3690
|
+
**Every session-bound request refuses at once while the client holds no connection.** `call`,
|
|
3691
|
+
`tools`, each `tasks/*` method reached through `client.tasks`, and a `listen` stream all carry
|
|
3692
|
+
the same refusal, because they all issue through one correlated-request door. A client that has
|
|
3693
|
+
never connected, or whose connection has ended, rejects with an `MCPError` carrying `-32600`
|
|
3694
|
+
and writes nothing to the transport. A stream refuses on its first `next()`, which is when a
|
|
3695
|
+
generator's body runs and therefore the first moment `listen` could refuse without making the
|
|
3696
|
+
stream eager. The alternative is the symptom this closes: a carrier that has already closed
|
|
3697
|
+
drops the frame without reporting it, so the caller waits out the whole request deadline —
|
|
3698
|
+
`DEFAULT_MCP_REQUEST_TIMEOUT`, 30 seconds, unless `timeout` shortened it — and is then told
|
|
3699
|
+
about a deadline rather than about the connection. A `connect` exempts its own round trips, and
|
|
3700
|
+
only while it is still the attempt this client is making: `connect` is emitted from inside the
|
|
3701
|
+
attempt, so a transport lost from a `connect` listener — a closed port, a stopped page server —
|
|
3702
|
+
supersedes that attempt at once, and every request issued after that loss refuses rather than
|
|
3703
|
+
riding an exemption the loss already ended. A `disconnect` from that same listener is the other
|
|
3704
|
+
sequence. It defers its teardown through the microtask queue, so the transport is still open
|
|
3705
|
+
when the listener's next request is issued: that request is admitted, and the teardown's own
|
|
3706
|
+
drain then rejects it with `MCP client disconnected`. It settles well inside its deadline rather
|
|
3707
|
+
than refusing, and it never parks. `discover` is outside the rule and stays available on a
|
|
3708
|
+
client that has never connected, because it is the probe a connection is built from.
|
|
3709
|
+
|
|
3530
3710
|
| Method | Returns | Summary |
|
|
3531
3711
|
| ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3532
3712
|
| `connect` | `Promise<void>` | Connects to the remote server — opens a connection on the transport and negotiates the modern wire revision. |
|
|
@@ -3808,6 +3988,38 @@ stdio.start() // a repeat arms nothing further — one reply per request
|
|
|
3808
3988
|
stdio.stop() // unbind, release stdin, and end this handle
|
|
3809
3989
|
```
|
|
3810
3990
|
|
|
3991
|
+
#### `ScopeInterface`
|
|
3992
|
+
|
|
3993
|
+
The hostable scope `createScopeServer` binds — `self` in a dedicated Web Worker or a Service
|
|
3994
|
+
Worker, or any object of this shape. No class implements it: the host does. It declares the
|
|
3995
|
+
members the scope server touches and stops there, so a real `self` satisfies it structurally
|
|
3996
|
+
while exposing far more.
|
|
3997
|
+
|
|
3998
|
+
| Method | Returns | Summary |
|
|
3999
|
+
| --------------------- | ------- | ---------------------------------------------------------------------------- |
|
|
4000
|
+
| `postMessage` | `void` | Posts one reply onto the scope's implicit channel. |
|
|
4001
|
+
| `addEventListener` | `void` | Subscribes to the scope's `message` events, portless and port-bearing alike. |
|
|
4002
|
+
| `removeEventListener` | `void` | Drops a `message` subscription. |
|
|
4003
|
+
|
|
4004
|
+
The fence under [Start a worker scope server](#start-a-worker-scope-server) drives a scope of
|
|
4005
|
+
exactly this shape, which is what lets that example round-trip a `tools/list` with no worker
|
|
4006
|
+
harness.
|
|
4007
|
+
|
|
4008
|
+
#### `ScopeTransportInterface`
|
|
4009
|
+
|
|
4010
|
+
The scope's duplex carrier, returned by `createScopeTransport` and bound by
|
|
4011
|
+
`createScopeServer`. It is an `MCPTransportInterface` — `send`, `listen`, `closed`, `close` —
|
|
4012
|
+
plus the one member the scope shape needs: the scope's own `message` listener has to push an
|
|
4013
|
+
inbound string into it, because the scope never registers `listen`'s handler itself.
|
|
4014
|
+
|
|
4015
|
+
| Method | Returns | Summary |
|
|
4016
|
+
| --------- | ------- | ------------------------------------------------------------------- |
|
|
4017
|
+
| `deliver` | `void` | Pushes one inbound message string into the active `listen` handler. |
|
|
4018
|
+
|
|
4019
|
+
The in-memory duplex channel under
|
|
4020
|
+
[Bind a server or a client to any duplex transport](#bind-a-server-or-a-client-to-any-duplex-transport)
|
|
4021
|
+
is the same shape, and its `deliver` is what the peer calls to hand a frame across.
|
|
4022
|
+
|
|
3811
4023
|
#### `ScopeServerInterface`
|
|
3812
4024
|
|
|
3813
4025
|
The worker-scope handle `createScopeServer` returns, and the browser twin of
|
|
@@ -3831,6 +4043,196 @@ worker.stop() // release every binding this call owns
|
|
|
3831
4043
|
worker.stop() // a repeat releases nothing further
|
|
3832
4044
|
```
|
|
3833
4045
|
|
|
4046
|
+
#### `PageServerInterface`
|
|
4047
|
+
|
|
4048
|
+
The in-page handle `createPageServer` returns. No class implements it: the factory owns the
|
|
4049
|
+
`MessageChannel`, the hosted `MCPServer`, and both bindings behind it, and publishes the client
|
|
4050
|
+
beside the one door that ends them. The `client` data member is bound and not connected —
|
|
4051
|
+
`connect` is yours to await, because it is a protocol round trip rather than a construction
|
|
4052
|
+
step.
|
|
4053
|
+
|
|
4054
|
+
The terminal is `stop`, the same verb `ScopeServerInterface` publishes for the same action, so
|
|
4055
|
+
a consumer who learned one factory reads the other without checking. The published `client`
|
|
4056
|
+
outlives the pair and is inert afterwards: every session-bound request — `call`, `tools`, each
|
|
4057
|
+
`tasks/*` method, and a `listen` stream — rejects at once with an `MCPError` carrying `-32600`,
|
|
4058
|
+
per [`MCPClientInterface`](#mcpclientinterface).
|
|
4059
|
+
|
|
4060
|
+
| Method | Returns | Summary |
|
|
4061
|
+
| ------ | ------- | ------------------------------------------------------------------------------- |
|
|
4062
|
+
| `stop` | `void` | Closes both ports, unbinds both sides, and disconnects the client — idempotent. |
|
|
4063
|
+
|
|
4064
|
+
This example hosts a server in the page, drives it, and shows that repeated cleanup is inert.
|
|
4065
|
+
|
|
4066
|
+
```ts
|
|
4067
|
+
import { createPageServer } from '@orkestrel/mcp/browser'
|
|
4068
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
4069
|
+
|
|
4070
|
+
const tools = createToolManager()
|
|
4071
|
+
tools.add(createTool({ name: 'add', execute: () => 5 }))
|
|
4072
|
+
|
|
4073
|
+
const page = createPageServer({ tools })
|
|
4074
|
+
await page.client.connect() // the pair negotiates over the channel; nothing touches the network
|
|
4075
|
+
page.stop() // both ports closed, both sides unbound, the client disconnected
|
|
4076
|
+
page.stop() // a repeat releases nothing further
|
|
4077
|
+
```
|
|
4078
|
+
|
|
4079
|
+
#### `ModelContextInterface`
|
|
4080
|
+
|
|
4081
|
+
The WebMCP bridge `createModelContext` returns, implemented by `ModelContext`. `publish` sends
|
|
4082
|
+
this page's tools out to `document.modelContext`; `adopt` brings that registry's tools back as
|
|
4083
|
+
`@orkestrel/tool` tools. The `emitter` data member republishes the registry's `toolchange` as
|
|
4084
|
+
`change`. Subscribe through `emitter.on`.
|
|
4085
|
+
|
|
4086
|
+
`publish` takes a snapshot **and** subscribes. The snapshot is taken when `publish` is called,
|
|
4087
|
+
before the work queues behind an earlier publication, so a manager mutated while a call waits
|
|
4088
|
+
its turn does not change what that call registers. The same call then subscribes to the
|
|
4089
|
+
manager's own `emitter` — `@orkestrel/tool` publishes `add`, `remove`, and `clear`. The
|
|
4090
|
+
document registry tracks the tool registry, and a consumer that changes the tool registry calls
|
|
4091
|
+
nothing.
|
|
4092
|
+
|
|
4093
|
+
**A followed change is a trigger, not a fact.** Each `add`, `remove`, and `clear` queues one
|
|
4094
|
+
synchronisation of the registrations this handle holds for that manager against what the
|
|
4095
|
+
manager holds when that queued work runs, so the bridge converges on the manager's state after
|
|
4096
|
+
every change it follows. One synchronisation covers every change that reaches it before it
|
|
4097
|
+
starts. A `publish` call made in between ends that cover: the publication runs after the queued
|
|
4098
|
+
synchronisation and prunes what it registered, so a change made after that call takes a
|
|
4099
|
+
synchronisation of its own, which runs after the publication. What the event carries cannot
|
|
4100
|
+
decide the outcome, and each of these cases is why: `remove` and `clear` name tools the manager
|
|
4101
|
+
no longer holds; a listener that ran earlier in the same dispatch can add a tool back under one
|
|
4102
|
+
of those names, with the same descriptor or another; the manager empties its map before it
|
|
4103
|
+
publishes `clear`, so such an addition stays in the manager; and `tools.destroy()` publishes
|
|
4104
|
+
`clear`, destroys its emitter, and then empties the map again, erasing an addition with no
|
|
4105
|
+
event left to report it. Reading the manager answers all of them the same way. A registration
|
|
4106
|
+
whose name the manager no longer advertises is aborted, and only registrations bound to that
|
|
4107
|
+
manager are — a name another manager's publication put there is left standing.
|
|
4108
|
+
|
|
4109
|
+
One manager is followed at a time. A `publish` naming another manager releases the subscription
|
|
4110
|
+
and takes up the new one, and `destroy` releases it outright. An event from a manager this
|
|
4111
|
+
handle no longer follows is ignored: releasing a subscription inside a dispatch does not
|
|
4112
|
+
withdraw the handler from the listener array that dispatch is walking, so a listener that
|
|
4113
|
+
republishes another manager leaves this handle's own handler still to run for a subscription
|
|
4114
|
+
that no longer exists. A followed change reaches no caller, so a tool advertising neither a
|
|
4115
|
+
`description` nor a `summary` is left unregistered rather than refusing anything, and a
|
|
4116
|
+
registration the document registry refuses is dropped, so the next `publish` or the next
|
|
4117
|
+
synchronisation registers that name again. Refusal is the caller's answer, and a followed
|
|
4118
|
+
change has no caller to give it to. A synchronisation registers only what it can carry, so such
|
|
4119
|
+
a tool standing under a name this handle already registered releases that registration instead
|
|
4120
|
+
of leaving it advertising a descriptor the manager no longer stands behind. Under a name this
|
|
4121
|
+
handle never registered the skip emits no `change`, because nothing reached the document
|
|
4122
|
+
registry; releasing a name this handle did register emits the registry's `change` like any
|
|
4123
|
+
other release. Compare the manager's own `definitions()` with what `adopt()` returns to read
|
|
4124
|
+
the mismatch, and `describeWebMCPTool` answers `undefined` for a name `definitions()` still
|
|
4125
|
+
lists, which is that mismatch read from the manager's own side.
|
|
4126
|
+
|
|
4127
|
+
Every name reconciles against what this handle registered for it, whether it arrives in a
|
|
4128
|
+
snapshot or through a synchronisation. The same manager still holding the same tool leaves the
|
|
4129
|
+
registration untouched, so a repeat registers nothing and the registry fires no `toolchange`
|
|
4130
|
+
reporting a change nobody made — and a descriptor JSON cannot encode, such as a cyclic
|
|
4131
|
+
`inputSchema`, is left alone rather than re-registered on every change to another name. Another
|
|
4132
|
+
tool of that manager advertising an equal descriptor leaves the registration standing too:
|
|
4133
|
+
execution routes through the manager by name, so the replacement's handler is already what a
|
|
4134
|
+
foreign agent reaches. A changed projection, or the same name arriving from a different
|
|
4135
|
+
manager, releases the registration this handle holds and registers the new descriptor bound to
|
|
4136
|
+
the new manager. A name the snapshot dropped is released, and so is a name the followed manager
|
|
4137
|
+
no longer holds — after everything the batch carries has reconciled, so the registry never
|
|
4138
|
+
withdraws a name while the tools replacing it are still being registered. A failed batch
|
|
4139
|
+
releases them too, because the prune runs whether or not every registration the batch asked
|
|
4140
|
+
for was made, and it still withdraws nothing the batch carries. Descriptor equality is
|
|
4141
|
+
structural and key-order-independent, so re-authoring one schema in another key order is not
|
|
4142
|
+
a change.
|
|
4143
|
+
|
|
4144
|
+
Every tool is projected before anything is registered, so a manager holding a tool that
|
|
4145
|
+
advertises neither a `description` nor a `summary` is refused whole, with an `MCPError`
|
|
4146
|
+
carrying `-32602` and naming the tool, and nothing registered.
|
|
4147
|
+
|
|
4148
|
+
**A registration's identity is its name, per document.** WebMCP keys the registry by tool name,
|
|
4149
|
+
so a later registration of a name replaces the earlier one whichever handle made it, and the
|
|
4150
|
+
release the earlier handle performs takes whatever now stands under that name. This is the
|
|
4151
|
+
whole of what `destroy` touching "only its own registrations" means: the handle aborts its own
|
|
4152
|
+
controllers, and a name it never registered is left alone, but a same-name registration another
|
|
4153
|
+
handle made later goes with the release. Publish overlapping names from one handle, or give
|
|
4154
|
+
each handle names of its own.
|
|
4155
|
+
|
|
4156
|
+
**A published tool's `execute` rejects with an `Error` carrying the failure's message.** The
|
|
4157
|
+
manager contains a throwing handler and reports `ToolFailure.error`, which is text, so the
|
|
4158
|
+
value that was thrown no longer exists to forward and a foreign caller receives a fresh
|
|
4159
|
+
`Error` whose `message` is that text.
|
|
4160
|
+
|
|
4161
|
+
**An adopted tool validates nothing.** It advertises the foreign `inputSchema` as its
|
|
4162
|
+
`parameters` and forwards the arguments it is given. Compiling that schema into a contract
|
|
4163
|
+
would let one page's unreadable or hostile schema refuse the whole `adopt` call, and the
|
|
4164
|
+
arguments reach a handler in another document that has to validate them anyway.
|
|
4165
|
+
|
|
4166
|
+
| Method | Returns | Summary |
|
|
4167
|
+
| --------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
4168
|
+
| `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. |
|
|
4170
|
+
| `destroy` | `void` | Aborts every registration this handle made, stops following the tool registry, and releases the emitter — idempotent. |
|
|
4171
|
+
|
|
4172
|
+
This example publishes a registry to a document that exposes WebMCP, reads the document's own
|
|
4173
|
+
tools back, and releases only what it registered.
|
|
4174
|
+
|
|
4175
|
+
```ts
|
|
4176
|
+
import { createModelContext } from '@orkestrel/mcp/browser'
|
|
4177
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
4178
|
+
|
|
4179
|
+
const tools = createToolManager()
|
|
4180
|
+
tools.add(createTool({ name: 'add', description: 'Adds two numbers', execute: () => 5 }))
|
|
4181
|
+
|
|
4182
|
+
const bridge = createModelContext() // undefined where the document exposes no registry
|
|
4183
|
+
if (bridge !== undefined) {
|
|
4184
|
+
bridge.emitter.on('change', () => console.log('the registry changed'))
|
|
4185
|
+
await bridge.publish(tools, { origins: ['https://partner.example'] })
|
|
4186
|
+
const foreign = await bridge.adopt()
|
|
4187
|
+
bridge.destroy() // aborts this handle's registrations and stops following `tools`
|
|
4188
|
+
}
|
|
4189
|
+
```
|
|
4190
|
+
|
|
4191
|
+
#### `WebMCPRegistryInterface`
|
|
4192
|
+
|
|
4193
|
+
The foreign registry itself, as `document.modelContext` exposes it, declared here because
|
|
4194
|
+
TypeScript's DOM library declares none of it. No class implements it: a user agent does, and
|
|
4195
|
+
`isWebMCPDocument` is what narrows a document onto it. The declaration carries the members the
|
|
4196
|
+
bridge dereferences and stops there, exactly as `ScopeInterface` carries what
|
|
4197
|
+
`createScopeServer` touches — a real registry satisfies it structurally and so does an
|
|
4198
|
+
IDL-faithful double. Reach it directly only to do something `ModelContextInterface` does not;
|
|
4199
|
+
the bridge is the supported path, and it is what owns the registration lifetime.
|
|
4200
|
+
|
|
4201
|
+
`executeTool` resolves `unknown` because the primary source disagrees with itself: the WebIDL
|
|
4202
|
+
types it `Promise<DOMString>` while the specification's own README sample answers the MCP
|
|
4203
|
+
content shape `{ content: [...] }`. See [WebMCP parity](#webmcp-parity) for that row.
|
|
4204
|
+
|
|
4205
|
+
| Method | Returns | Summary |
|
|
4206
|
+
| --------------------- | ------------------------------------------ | --------------------------------------------------------------------- |
|
|
4207
|
+
| `registerTool` | `Promise<void>` | Registers one tool, resolving when the registry has accepted it. |
|
|
4208
|
+
| `getTools` | `Promise<readonly WebMCPRegisteredTool[]>` | Reads the registered tools this document may see. |
|
|
4209
|
+
| `executeTool` | `Promise<unknown>` | Runs one registered tool and resolves whatever its callback returned. |
|
|
4210
|
+
| `addEventListener` | `void` | Subscribes to the registry's `toolchange` event. |
|
|
4211
|
+
| `removeEventListener` | `void` | Drops a `toolchange` subscription. |
|
|
4212
|
+
|
|
4213
|
+
This example drives the registry directly: it subscribes, registers a tool, reads the registry
|
|
4214
|
+
back, runs one entry, releases the subscription, and unregisters by aborting the registration
|
|
4215
|
+
signal — which is WebMCP's own removal path.
|
|
4216
|
+
|
|
4217
|
+
```ts
|
|
4218
|
+
import { isWebMCPDocument } from '@orkestrel/mcp/browser'
|
|
4219
|
+
|
|
4220
|
+
if (isWebMCPDocument(document)) {
|
|
4221
|
+
const registry = document.modelContext
|
|
4222
|
+
const controller = new AbortController()
|
|
4223
|
+
const onChange = () => console.log('the registry changed')
|
|
4224
|
+
registry.addEventListener('toolchange', onChange)
|
|
4225
|
+
await registry.registerTool(
|
|
4226
|
+
{ name: 'add', description: 'Adds two numbers', execute: async () => '5' },
|
|
4227
|
+
{ exposedTo: ['https://partner.example'], signal: controller.signal },
|
|
4228
|
+
)
|
|
4229
|
+
const [registered] = await registry.getTools()
|
|
4230
|
+
if (registered !== undefined) await registry.executeTool(registered, { x: 2, y: 3 })
|
|
4231
|
+
registry.removeEventListener('toolchange', onChange)
|
|
4232
|
+
controller.abort() // the unregistration path: aborting the registration signal
|
|
4233
|
+
}
|
|
4234
|
+
```
|
|
4235
|
+
|
|
3834
4236
|
## Patterns
|
|
3835
4237
|
|
|
3836
4238
|
### Expose a tool registry over MCP
|
|
@@ -3868,6 +4270,72 @@ const listed = await server.handle(
|
|
|
3868
4270
|
// listed → '{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"search","inputSchema":{"type":"object"},"description":"Search the docs"},{"name":"add","inputSchema":{"type":"object"}}],"resultType":"complete","ttlMs":60000,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"docs","version":"1.0.0"}}}}'
|
|
3869
4271
|
```
|
|
3870
4272
|
|
|
4273
|
+
### Refresh the tools an agent holds
|
|
4274
|
+
|
|
4275
|
+
The server produces each tools notification directly from its registry. A server-side `add`
|
|
4276
|
+
triggers this refresh without a consumer producer or a second notification written by the
|
|
4277
|
+
application. Apply each snapshot between agent runs. The refresh owns the names it installed
|
|
4278
|
+
from the client and replaces or removes tools by name. You must not register a local tool under a
|
|
4279
|
+
name the refresh installed. A collision with a local tool records the remote name and
|
|
4280
|
+
keeps the local tool. A failed fetch records the error and leaves the last snapshot installed.
|
|
4281
|
+
Open the subscription before fetching the initial snapshot so changes during that fetch stay
|
|
4282
|
+
queued. The loop processes notifications serially and performs no polling.
|
|
4283
|
+
|
|
4284
|
+
The following example uses an already-connected `client`. The application owns when this loop
|
|
4285
|
+
runs relative to its agent and aborts `subscription` when it stops consuming notifications.
|
|
4286
|
+
|
|
4287
|
+
```ts
|
|
4288
|
+
import type { MCPClientInterface } from '@orkestrel/mcp'
|
|
4289
|
+
import type { ToolInterface, ToolManagerInterface } from '@orkestrel/tool'
|
|
4290
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
4291
|
+
|
|
4292
|
+
export interface ToolRefreshResult {
|
|
4293
|
+
readonly installed: readonly string[]
|
|
4294
|
+
readonly collisions: readonly string[]
|
|
4295
|
+
readonly failures: readonly unknown[]
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
export async function refreshTools(
|
|
4299
|
+
client: MCPClientInterface,
|
|
4300
|
+
tools: ToolManagerInterface,
|
|
4301
|
+
installed: readonly string[],
|
|
4302
|
+
): Promise<ToolRefreshResult> {
|
|
4303
|
+
const collisions: string[] = []
|
|
4304
|
+
let snapshot: readonly ToolInterface[]
|
|
4305
|
+
try {
|
|
4306
|
+
snapshot = await client.tools()
|
|
4307
|
+
} catch (error) {
|
|
4308
|
+
return { installed, collisions, failures: [error] }
|
|
4309
|
+
}
|
|
4310
|
+
const accepted = snapshot.filter((tool) => {
|
|
4311
|
+
if (tools.tool(tool.name) !== undefined && !installed.includes(tool.name)) {
|
|
4312
|
+
collisions.push(tool.name)
|
|
4313
|
+
return false
|
|
4314
|
+
}
|
|
4315
|
+
return true
|
|
4316
|
+
})
|
|
4317
|
+
tools.remove(installed)
|
|
4318
|
+
tools.add(accepted)
|
|
4319
|
+
return { installed: accepted.map((tool) => tool.name), collisions, failures: [] }
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
const tools = createToolManager()
|
|
4323
|
+
tools.add(createTool({ name: 'local', execute: () => 'local value' }))
|
|
4324
|
+
const subscription = new AbortController()
|
|
4325
|
+
const notifications = client.listen({ toolsListChanged: true }, { signal: subscription.signal })
|
|
4326
|
+
try {
|
|
4327
|
+
await notifications.next() // Consume the subscription acknowledgement.
|
|
4328
|
+
let outcome = await refreshTools(client, tools, [])
|
|
4329
|
+
for await (const notification of notifications) {
|
|
4330
|
+
if (notification.method === 'notifications/tools/list_changed') {
|
|
4331
|
+
outcome = await refreshTools(client, tools, outcome.installed)
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
} finally {
|
|
4335
|
+
subscription.abort()
|
|
4336
|
+
}
|
|
4337
|
+
```
|
|
4338
|
+
|
|
3871
4339
|
### Drive the typed core directly
|
|
3872
4340
|
|
|
3873
4341
|
When the request is already parsed (a test, an in-process bridge), call
|
|
@@ -4318,8 +4786,47 @@ closed — while ordinary upstream completion closes the response without invent
|
|
|
4318
4786
|
- [HTTP response lifecycle composition](../tests/src/server/HTTPDisconnect.test.ts)
|
|
4319
4787
|
- [HTTP handler integration](../tests/src/server/handlers.test.ts)
|
|
4320
4788
|
- [Session middleware integration](../tests/src/server/middlewares.test.ts)
|
|
4321
|
-
- [Guide/source/public-barrel parity; legacy-removability and public-face boundaries; native guide-input, fence-language, summary, titled-example, and README-pitch checks; what the spawned stdio child receives; how the composed stdio server answers a legacy `initialize`; and the client subscription, progress, and
|
|
4789
|
+
- [Guide/source/public-barrel parity; legacy-removability and public-face boundaries; native guide-input, fence-language, summary, titled-example, and README-pitch checks; what the spawned stdio child receives; how the composed stdio server answers a legacy `initialize`; and the client subscription, progress, transport, and Refresh the tools an agent holds demonstrations](../tests/guides.test.ts)
|
|
4322
4790
|
- [The packed artifact a consumer installs, across its faces and its ESM and CommonJS builds](../tests/distribution.test.ts)
|
|
4791
|
+
- [The packed artifact composed with the installed agent, tool, and parser artifacts in a real Chromium page](../tests/distribution.test.ts)
|
|
4792
|
+
|
|
4793
|
+
The `distribution` project runs from `prepublishOnly` as
|
|
4794
|
+
`npm run test:distribution -- --mode release`. Under `--mode release` the proof fails when it
|
|
4795
|
+
cannot reach the registry or a browser; anywhere else it skips and names what it could not
|
|
4796
|
+
reach.
|
|
4797
|
+
|
|
4798
|
+
Beside the checks that read this package's published surface, that project composes its packed
|
|
4799
|
+
artifact with the installed `@orkestrel/agent`, `@orkestrel/tool`, and `@orkestrel/ndjson`
|
|
4800
|
+
artifacts inside a real Chromium page, served by the isolated consumer's own Node fixture. The
|
|
4801
|
+
page loads every one of those artifacts over an import map answered from the consumer's own
|
|
4802
|
+
`node_modules`, so no bundler stands between the published files and the page. The recorders arm
|
|
4803
|
+
after the page and its modules have loaded — the browser's request log and a counter around
|
|
4804
|
+
the page's global transport — so each reading covers the composition alone. These are the
|
|
4805
|
+
receipts that project reads:
|
|
4806
|
+
|
|
4807
|
+
- `evaluates every @orkestrel entry the installed agent imports` — the page evaluates each root
|
|
4808
|
+
entry the installed agent's own module names, and every one of them publishes a defined
|
|
4809
|
+
export.
|
|
4810
|
+
- `runs a page tool through an installed agent with no request at all` — an agent driven by a
|
|
4811
|
+
scripted provider dispatches a tool that writes into the document, and neither recorder
|
|
4812
|
+
reports a request.
|
|
4813
|
+
- `completes an in-page MCP pair with no request at all` — `createPageServer` connects, lists,
|
|
4814
|
+
and calls, the listing carries the tool's `title` and its `pure` annotation back off the wire,
|
|
4815
|
+
and the client its `stop` leaves behind refuses a later call with `-32600`.
|
|
4816
|
+
- `dispatches an agent call into the page server with no request at all` — an agent whose
|
|
4817
|
+
registry holds the pair's tools runs one in the hosted server, the registry's own entry keeps
|
|
4818
|
+
that `title` and annotation, and a name that server does not hold comes back as a failed tool
|
|
4819
|
+
result.
|
|
4820
|
+
- `carries a caller abort into the page server handler` — `abort` on the agent's run reaches the
|
|
4821
|
+
hosted handler's own execution-context signal.
|
|
4822
|
+
- `spends one relay request per model turn and runs the tool in the page` — an agent over
|
|
4823
|
+
`createRelayProvider` against a `createRelay` fixture on `127.0.0.1` executes its page tool in
|
|
4824
|
+
the page.
|
|
4825
|
+
- `refuses a relay turn presenting a credential the fixture does not hold` — the installed relay
|
|
4826
|
+
refuses a credential its `authorize` callback rejects, answering `401`, and the route records
|
|
4827
|
+
the request it refused.
|
|
4828
|
+
- `reports one deliberate request on the request log and the counter` — the control that shows
|
|
4829
|
+
the request log and the counter report traffic.
|
|
4323
4830
|
|
|
4324
4831
|
## Declared non-goals
|
|
4325
4832
|
|
|
@@ -4391,6 +4898,37 @@ the session middleware). A deployment that already validates origin upstream say
|
|
|
4391
4898
|
`origin: { enabled: false }` rather than by passing an empty list, because delegation
|
|
4392
4899
|
is a different decision from an empty allowlist and deserves its own word.
|
|
4393
4900
|
|
|
4901
|
+
## WebMCP parity
|
|
4902
|
+
|
|
4903
|
+
WebMCP puts a tool registry on the document — `document.modelContext` — so a page can offer its
|
|
4904
|
+
own capabilities to an agent running in the browser. It is a different door onto the same idea
|
|
4905
|
+
this package serves over a wire, so each surface it defines is compared here against what this
|
|
4906
|
+
package publishes, and every row ends implement, retain, or exclude with the source it was read
|
|
4907
|
+
from. `G5c` is the verbatim WebIDL, samples, and chromestatus reading taken from the
|
|
4908
|
+
[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 |
|
|
4931
|
+
|
|
4394
4932
|
## Declared conformance gaps
|
|
4395
4933
|
|
|
4396
4934
|
The conformance project is a foreign-runner exchange and a schema-authority comparison,
|
|
@@ -4645,17 +5183,6 @@ after its caller has gone. **The consumer's options:** bound the tool itself, or
|
|
|
4645
5183
|
a held-open `MCPStream` from a registered method so the keepalive seam applies.
|
|
4646
5184
|
**Closer:** none named; the limit is structural to a unary HTTP response.
|
|
4647
5185
|
|
|
4648
|
-
**A tool run through the default registry cannot observe cancellation.** Dispatch resolves
|
|
4649
|
-
one signal per request and hands it to every method, selector, principal resolver, and
|
|
4650
|
-
subscription producer the request reaches — but the default execution path calls
|
|
4651
|
-
`ToolManagerInterface.execute(call)`, whose signature takes a call and nothing else. There is
|
|
4652
|
-
no seam to hand a signal through, so a server configured without `execution` runs its tool to
|
|
4653
|
-
completion after the request that asked for it has ended, and abandons the result. **What it
|
|
4654
|
-
costs:** a long or expensive tool keeps spending after its caller is gone. **The consumer's
|
|
4655
|
-
options:** supply `MCPServerOptions.execution`, whose `MCPExecutionContext` carries `signal`
|
|
4656
|
-
and can stop the work; or bound the tool itself. **Closer:** none inside this package — the
|
|
4657
|
-
limit is in the `execute` signature, which `@orkestrel/tool` owns.
|
|
4658
|
-
|
|
4659
5186
|
**A producer that ignores its signal cannot be forced to finish.** A controlled stream
|
|
4660
5187
|
settles its consumer promptly whatever the producer is doing, and aborts the request's
|
|
4661
5188
|
signal before delegating cleanup — but JavaScript cannot settle work a generator is
|
|
@@ -4670,9 +5197,9 @@ frame as the message-based cancellation path for the transports that still have
|
|
|
4670
5197
|
WebSocket, and `MessagePort`. `bindServer` holds one `AbortController` per live request, keyed
|
|
4671
5198
|
by request id and retired whenever that request leaves, and supplies its signal to `handle`,
|
|
4672
5199
|
so an inbound cancellation aborts the named request and the cancelled request writes no
|
|
4673
|
-
response.
|
|
4674
|
-
`
|
|
4675
|
-
|
|
5200
|
+
response. The default execution path forwards that signal through the tool context. A custom
|
|
5201
|
+
`MCPServerOptions.execution` handler receives `signal` and `caller` and forwards them
|
|
5202
|
+
when delegating to `ToolManagerInterface.execute`. **What remains:** on
|
|
4676
5203
|
Streamable HTTP there is no such frame at all — there, closing the response stream is the
|
|
4677
5204
|
cancellation signal, and only a streamed response has one to close. **Closer:** none possible
|
|
4678
5205
|
for the HTTP face; the limit is the dated revision's.
|
|
@@ -4696,7 +5223,9 @@ server `SHOULD` send the empty `subscriptions/listen` result to signal a gracefu
|
|
|
4696
5223
|
attributes the notification to the client alone. The schema carries only the generic
|
|
4697
5224
|
`CancelledNotification` with `requestId` and an optional `reason` — no subscription-specific
|
|
4698
5225
|
field or variant — so it corroborates neither page. This server sends the empty result,
|
|
4699
|
-
correlated by the original request id through `buildSubscriptionResult`, on every transport
|
|
5226
|
+
correlated by the original request id through `buildSubscriptionResult`, on every transport
|
|
5227
|
+
when a consumer producer ends and the honoured filter omits `toolsListChanged`. A stream
|
|
5228
|
+
honouring the tools family has no graceful end; it stays open until failure or signal abort.
|
|
4700
5229
|
**What it costs:** a client written against the cancellation page, watching for a notification
|
|
4701
5230
|
it believes is required, sees the result instead. **Do not "fix" this toward the cancellation
|
|
4702
5231
|
page** — emitting the notification as well would send a frame the governing page does not
|
|
@@ -4748,11 +5277,31 @@ The modern-only scope of `subscriptions/listen` is a stated limit rather than a
|
|
|
4748
5277
|
it is recorded under [Declared non-goals](#declared-non-goals) with the other era-scoped
|
|
4749
5278
|
surfaces.
|
|
4750
5279
|
|
|
5280
|
+
**`document.modelContext` is proven against an IDL-faithful double alone.** `createModelContext`,
|
|
5281
|
+
`ModelContext`, and the `WebMCP*` wire types translate the registry the
|
|
5282
|
+
[WebMCP specification](https://webmachinelearning.github.io/webmcp) defines, and the
|
|
5283
|
+
chromestatus record, read 2026-09-15 and last updated 2026-08-12, reports that feature as
|
|
5284
|
+
`Proposed` with `"flag": false` and `"origintrial": false` — no browser ships it, behind a flag or in an origin trial. The real
|
|
5285
|
+
Chromium the browser suite drives therefore exposes no `document.modelContext`, which
|
|
5286
|
+
`tests/src/browser/factories.test.ts` records as its own reading of the page rather than
|
|
5287
|
+
assuming, and every bridge scenario runs against `tests/fixtures/modelContext.ts`: an in-memory
|
|
5288
|
+
implementation of that WebIDL, member for member and nothing beyond it. **What that proves:**
|
|
5289
|
+
the translation each way — the projected annotations, the exposure and origin options, the
|
|
5290
|
+
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
|
|
5292
|
+
agent's own registry accepts these registrations, or that an agent driving one reaches this
|
|
5293
|
+
page's tools. **What it costs:** nothing a consumer can reach today, because
|
|
5294
|
+
`createModelContext` answers `undefined` on every shipping browser, so feature absence stays
|
|
5295
|
+
absence instead of becoming a passing integration claim. **Closer:** a browser that ships the
|
|
5296
|
+
registry, after which the same scenarios run against the real global and this entry names the
|
|
5297
|
+
version that closed it.
|
|
5298
|
+
|
|
4751
5299
|
**Not every guide fence is executed.** `tests/guides.test.ts` transcribes and drives the
|
|
4752
5300
|
flagship ones: the `tools/list` metadata pair, the stdio child's merged environment, its piped
|
|
4753
5301
|
stderr and the retained evidence tail, the composed stdio server's legacy handshake, the
|
|
4754
|
-
consumer-visible client's modern version,
|
|
4755
|
-
capacity refusal
|
|
5302
|
+
consumer-visible client's modern version, the subscription stream's delivery order and
|
|
5303
|
+
capacity refusal, and `Refresh the tools an agent holds`. Every other fence carries
|
|
5304
|
+
named-import, symbol, and link parity alone. **What
|
|
4756
5305
|
it costs:** a fence whose comment claims a value nothing asserts is checked for its names and
|
|
4757
5306
|
not for its answer, so a behaviour that drifts under one of those fences reddens no gate.
|
|
4758
5307
|
**Closer:** a transcription per fence, added with the claim it pins.
|
|
@@ -4878,9 +5427,10 @@ capabilities: { tools: {} }, serverInfo: { name, version } }`, the version negot
|
|
|
4878
5427
|
absent one to the shared frozen `EMPTY_MCP_ARGUMENTS`),
|
|
4879
5428
|
narrowed with `@orkestrel/contract`'s guards (no `as`); a missing /
|
|
4880
5429
|
non-string `name` → a `-32602` invalid-params error. Otherwise it runs
|
|
4881
|
-
`tools.execute(
|
|
4882
|
-
|
|
4883
|
-
|
|
5430
|
+
`tools.execute(call, context)` under the modern and legacy wire eras. The call carries
|
|
5431
|
+
`{ id, name, arguments }`; the context carries the request's `signal` and the optional
|
|
5432
|
+
`caller` from `options.caller`, omitted when absent. The tool body receives that context.
|
|
5433
|
+
Because the `ToolManager`
|
|
4884
5434
|
(`@orkestrel/tool`) already isolates a thrown tool (and an unknown name)
|
|
4885
5435
|
into a `success: false` result, the server adds no try/catch: that branch's
|
|
4886
5436
|
`error` maps to `{ content: [{ type: 'text', text: <error> }], isError: true }`;
|
|
@@ -4916,9 +5466,11 @@ JSON.stringify(value) }], structuredContent: value }`, carrying the value unchan
|
|
|
4916
5466
|
requires `params.notifications`; the server acknowledges the exact intersection
|
|
4917
5467
|
with its configured support, and that acknowledgement is the first message
|
|
4918
5468
|
carrying this request's reserved subscription id. Every delivered notification
|
|
4919
|
-
carries the same stamp. Ending the
|
|
5469
|
+
carries the same stamp. Ending the consumer's producer closes gracefully when the
|
|
5470
|
+
honoured filter omits `toolsListChanged`, with
|
|
4920
5471
|
`{ resultType: 'complete', _meta: { 'io.modelcontextprotocol/subscriptionId': id,
|
|
4921
|
-
… } }`.
|
|
5472
|
+
… } }`. A stream honouring the tools family stays open until failure or signal abort.
|
|
5473
|
+
The request id is only stream identity: a later request does not supersede
|
|
4922
5474
|
an earlier one. The legacy method remains absent and answers `-32601`.
|
|
4923
5475
|
7. **`handle` maps the boundary failures.** A `JSON.parse` throw (malformed
|
|
4924
5476
|
JSON) → a serialized `-32700` (Parse error) response with no `id` member; a
|