@orkestrel/mcp 0.0.29 → 0.0.31

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.
@@ -1,12 +1,70 @@
1
+ import type { EmitterErrorHandler } from '@orkestrel/emitter';
2
+ import type { EmitterHooks } from '@orkestrel/emitter';
1
3
  import type { EmitterInterface } from '@orkestrel/emitter';
2
4
  import type { HTTPClientTransportOptions } from '@orkestrel/mcp';
3
5
  import type { JSONRPCMessage } from '@orkestrel/mcp';
6
+ import type { MCPClientInterface } from '@orkestrel/mcp';
7
+ import type { MCPClientOptions } from '@orkestrel/mcp';
4
8
  import type { MCPMessageTransportEventMap } from '@orkestrel/mcp';
5
9
  import type { MCPMessageTransportInterface } from '@orkestrel/mcp';
6
10
  import type { MCPServerInterface } from '@orkestrel/mcp';
7
11
  import type { MCPTransportInterface } from '@orkestrel/mcp';
12
+ import type { ToolAnnotations } from '@orkestrel/tool';
13
+ import type { ToolDefinition } from '@orkestrel/tool';
14
+ import type { ToolInterface } from '@orkestrel/tool';
8
15
  import type { ToolManagerInterface } from '@orkestrel/tool';
9
16
 
17
+ /**
18
+ * Builds the WebMCP projection of every tool a registry advertises, or refuses the batch.
19
+ *
20
+ * @remarks
21
+ * The WebMCP twin of `@orkestrel/mcp`'s `buildToolDescriptors`. Each tool is projected through
22
+ * `@orkestrel/tool`'s own `toolToDefinition` — the projection `definitions()` applies — so a
23
+ * tool advertises one description across the MCP wire and the WebMCP registry alike. The tool
24
+ * travels beside its descriptor because a registration records which tool it was made for, and
25
+ * a descriptor cannot report that.
26
+ *
27
+ * It refuses rather than skips. WebMCP requires `description`, and each alternative to
28
+ * refusing is worse: an empty string is an invented value a foreign agent reads as a real one,
29
+ * and silently dropping the tool publishes a registry missing a tool its author asked for.
30
+ * Refusing before any registration happens is also what keeps `publish` atomic — nothing is
31
+ * registered when one tool cannot be.
32
+ *
33
+ * @param manager - The tool registry to project
34
+ * @returns One projection per advertised tool, in registry order
35
+ * @throws Thrown as an `MCPError` carrying `-32602` when a tool advertises no description,
36
+ * naming the tool
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const tools = createToolManager()
41
+ * tools.add(createTool({ name: 'add', description: 'Adds two numbers', execute: () => 5 }))
42
+ * buildWebMCPProjections(tools).map((projection) => projection.descriptor.name) // ['add']
43
+ * ```
44
+ */
45
+ export declare function buildWebMCPProjections(manager: ToolManagerInterface): readonly WebMCPProjection[];
46
+
47
+ /**
48
+ * Collects the WebMCP projection of every tool a registry advertises that WebMCP can carry.
49
+ *
50
+ * @remarks
51
+ * The skipping sibling of {@link buildWebMCPProjections}, and the reading a followed change
52
+ * reconciles against: a followed change reaches no caller, so a tool advertising neither a
53
+ * `description` nor a `summary` is left out of the collection rather than refusing a batch
54
+ * nobody asked for.
55
+ *
56
+ * @param manager - The tool registry to project
57
+ * @returns One projection per advertised tool WebMCP can carry, in registry order
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const tools = createToolManager()
62
+ * tools.add(createTool({ name: 'bare', execute: () => 1 }))
63
+ * collectWebMCPProjections(tools) // []
64
+ * ```
65
+ */
66
+ export declare function collectWebMCPProjections(manager: ToolManagerInterface): readonly WebMCPProjection[];
67
+
10
68
  /**
11
69
  * Creates the HTTP client transport for an
12
70
  * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPMessageTransportInterface}
@@ -82,6 +140,91 @@ export declare function createHTTPClientTransport(options: HTTPClientTransportOp
82
140
  */
83
141
  export declare function createMessagePortTransport(options: MessagePortTransportOptions): MCPTransportInterface;
84
142
 
143
+ /**
144
+ * Creates the bridge between a tool registry and a document's WebMCP registry, or reports that
145
+ * the document exposes none.
146
+ *
147
+ * @remarks
148
+ * Feature detection is the return value: `undefined` means this document has no
149
+ * `document.modelContext`, which is the reading every browser gives today — the specification
150
+ * is incubating in a Community Group, and the chromestatus record, read 2026-09-15 and last
151
+ * updated 2026-08-12, reports `Proposed` with `"flag": false` and `"origintrial": false`. There
152
+ * is no `supported` flag to read and no polyfill behind the factory, because a local
153
+ * implementation of an absent platform feature is one a caller mistakes for the platform.
154
+ *
155
+ * The bridge borrows the registry. It aborts only the registrations it made, so a name it
156
+ * never registered is left exactly as it found it. WebMCP keys a registration by tool name per
157
+ * document, so releasing a name releases whatever now stands under it — a same-name
158
+ * registration the page or another bridge made later goes with it.
159
+ *
160
+ * @param options - The document to bridge and the emitter's initial wiring; see
161
+ * {@link ModelContextOptions}
162
+ * @returns A {@link ModelContextInterface}, or `undefined` when the document exposes no
163
+ * WebMCP registry
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * import { createModelContext } from '@orkestrel/mcp/browser'
168
+ * import { createToolManager } from '@orkestrel/tool'
169
+ *
170
+ * const bridge = createModelContext()
171
+ * if (bridge !== undefined) {
172
+ * await bridge.publish(createToolManager())
173
+ * const foreign = await bridge.adopt()
174
+ * bridge.destroy()
175
+ * }
176
+ * ```
177
+ */
178
+ export declare function createModelContext(options?: ModelContextOptions): ModelContextInterface | undefined;
179
+
180
+ /**
181
+ * Creates an `MCPServer` hosted inside the calling page and hands back the client bound to it
182
+ * — the page twin of {@link createScopeServer}, and the in-page MCP pair as one call.
183
+ *
184
+ * @remarks
185
+ * The pair is a native `MessageChannel`: the server binds `port1`, the client drives `port2`,
186
+ * and no byte leaves the page. That is the point of the factory — a consumer assembling it by
187
+ * hand writes the channel, two transports, `bindServer`, `createDuplexClientTransport`,
188
+ * `createMCPClient`, and `bindClient`, in an order {@link MessagePortTransport}'s own doc warns
189
+ * about: a `MessagePort` starts dispatching at construction, so an `await` interleaved between
190
+ * a transport and its binder drops whatever arrived in the gap. This factory never suspends
191
+ * between the two.
192
+ *
193
+ * The returned client is bound but not connected. Connection is a protocol round trip, so it
194
+ * stays the consumer's `await client.connect()` rather than a promise this call hides — and a
195
+ * factory that returned a promise could not return the terminal beside it.
196
+ *
197
+ * `stop` closes the client's port first, so the client observes the close and reports
198
+ * `connected` as `false` with its pending requests rejected, then unbinds both sides and
199
+ * closes the server's port. It is idempotent, and it takes the twin's verb because it is the
200
+ * twin's action: {@link createScopeServer} publishes `stop` for ending a hosted server's
201
+ * bindings, and a consumer who learned one factory reads the other without checking.
202
+ *
203
+ * The published `client` outlives the pair and is inert after `stop`: every session-bound
204
+ * request issued on it — `call`, `tools`, each `tasks/*` method, and a `listen` stream on its
205
+ * first `next()` — rejects at once with an `MCPError` carrying `-32600`, rather than waiting
206
+ * out its request deadline against a channel nothing is listening on.
207
+ *
208
+ * @param options - The tools, the optional server identity, and the optional client settings;
209
+ * see {@link PageServerOptions}
210
+ * @returns A {@link PageServerInterface} holding the bound client and the pair's `stop`
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * import { createPageServer } from '@orkestrel/mcp/browser'
215
+ * import { createTool, createToolManager } from '@orkestrel/tool'
216
+ *
217
+ * const tools = createToolManager()
218
+ * tools.add(createTool({ name: 'add', execute: () => 5 }))
219
+ *
220
+ * const page = createPageServer({ tools })
221
+ * await page.client.connect()
222
+ * const value = await page.client.call('add', {}) // { resultType: 'complete', value: 5 }
223
+ * page.stop()
224
+ * ```
225
+ */
226
+ export declare function createPageServer(options: PageServerOptions): PageServerInterface;
227
+
85
228
  /**
86
229
  * Builds {@link createScopeServer}'s `message`-event listener — the unified dispatcher that
87
230
  * routes every inbound event on a hostable scope, portless or port-bearing, to the right
@@ -226,6 +369,96 @@ export declare const DEFAULT_MCP_SERVER_NAME = "@orkestrel/mcp";
226
369
  /** Supplies the default server version `createScopeServer` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
227
370
  export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
228
371
 
372
+ /**
373
+ * Projects the descriptor a registry advertises for one tool name, or reports that it has none.
374
+ *
375
+ * @remarks
376
+ * Reads the manager's own `definitions()` rather than a tool instance, so the description here
377
+ * is the one the registry advertises — an authored `summary` in place of the full
378
+ * `description` — and one tool reaches the WebMCP registry and the MCP wire describing itself
379
+ * the same way.
380
+ *
381
+ * `undefined` covers both answers a caller must not conflate with a descriptor: the manager
382
+ * advertises no tool under that name, and the tool it advertises carries no description, which
383
+ * is the member WebMCP requires.
384
+ *
385
+ * @param manager - The tool registry to read
386
+ * @param name - The tool name to describe
387
+ * @returns The WebMCP descriptor, or `undefined` when the registry advertises none
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * const tools = createToolManager()
392
+ * tools.add(createTool({ name: 'add', description: 'Adds two numbers', execute: () => 5 }))
393
+ * describeWebMCPTool(tools, 'add')?.description // 'Adds two numbers'
394
+ * ```
395
+ */
396
+ export declare function describeWebMCPTool(manager: ToolManagerInterface, name: string): WebMCPDescriptor | undefined;
397
+
398
+ /**
399
+ * Determines whether an unknown value is a document exposing the WebMCP tool registry.
400
+ *
401
+ * @remarks
402
+ * This is the feature detection {@link import('./factories.js').createModelContext} performs,
403
+ * published so a consumer can run it before deciding to build a bridge at all. It reads
404
+ * `modelContext` and checks it with {@link isWebMCPRegistry}; it asserts nothing about the rest
405
+ * of a `Document`, because that member is the whole of what the bridge needs.
406
+ *
407
+ * @param value - The unknown value to inspect
408
+ * @returns True if the value carries a WebMCP registry; false otherwise
409
+ *
410
+ * @example
411
+ * ```ts
412
+ * isWebMCPDocument(globalThis.document) // false in a browser that ships no WebMCP
413
+ * ```
414
+ */
415
+ export declare function isWebMCPDocument(value: unknown): value is WebMCPDocument;
416
+
417
+ /**
418
+ * Determines whether an unknown value is a WebMCP tool registry.
419
+ *
420
+ * @remarks
421
+ * Reads the members the bridge dereferences — the IDL's `registerTool`, `getTools`, and
422
+ * `executeTool` operations, plus the `EventTarget` pair the `toolchange` subscription needs —
423
+ * and nothing else. A registry carrying extra members is still a registry, and a user agent's
424
+ * own implementation reaches every one of these through its prototype.
425
+ *
426
+ * @param value - The unknown value to inspect
427
+ * @returns True if the value exposes every WebMCP registry operation; false otherwise
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * isWebMCPRegistry({}) // false
432
+ * ```
433
+ */
434
+ export declare function isWebMCPRegistry(value: unknown): value is WebMCPRegistryInterface;
435
+
436
+ /**
437
+ * Determines whether two WebMCP descriptors advertise the same tool to the registry.
438
+ *
439
+ * @remarks
440
+ * The reading `ModelContextInterface.publish` reconciles a name against once the manager's
441
+ * tool has changed under it: equal descriptors leave the live registration standing, because
442
+ * execution routes through the manager by name, and anything else releases it and registers
443
+ * the new one.
444
+ *
445
+ * Equality is structural and key-order-independent, through `@orkestrel/contract`'s
446
+ * `canonicalStringify`: `inputSchema` is the author's own JSON Schema record, and two
447
+ * authorings of the same schema that differ only in key order describe the same tool. A
448
+ * descriptor JSON cannot encode — a cyclic or unreadable `inputSchema` — is reported as
449
+ * unequal, which re-registers rather than serving a descriptor nothing could compare.
450
+ *
451
+ * @param held - The descriptor the live registration carries
452
+ * @param projected - The descriptor this publication projected
453
+ * @returns True when both describe the same tool; false otherwise
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * matchesDescriptor({ name: 'add', description: 'Adds' }, { description: 'Adds', name: 'add' }) // true
458
+ * ```
459
+ */
460
+ export declare function matchesDescriptor(held: WebMCPDescriptor, projected: WebMCPDescriptor): boolean;
461
+
229
462
  /**
230
463
  * Carries the Model Context Protocol over a native `MessagePort` from the browser face — a
231
464
  * {@link MCPTransportInterface}, the genuinely new capability this face adds: MCP over
@@ -312,6 +545,308 @@ export declare interface MessagePortTransportOptions {
312
545
  readonly port: MessagePort;
313
546
  }
314
547
 
548
+ /**
549
+ * Bridges a `ToolManagerInterface` and a document's WebMCP tool registry — the
550
+ * {@link ModelContextInterface} {@link import('./factories.js').createModelContext} returns.
551
+ *
552
+ * @remarks
553
+ * - **It borrows the registry, it does not own it.** The handle registers tools, retains one
554
+ * `AbortController` per registration, and aborts exactly those on `destroy`. WebMCP's own
555
+ * unregistration path is that abort. Registration identity is the tool name, per document,
556
+ * so releasing a name releases whatever now stands under it — including a same-name
557
+ * registration another handle made later.
558
+ * - **`publish` snapshots at the call, then follows the manager.** The manager is projected
559
+ * when `publish` is called, before the work queues behind an earlier publication, so a
560
+ * registry mutated while this call waits its turn does not decide what this call registers.
561
+ * The same call subscribes to the manager's own `emitter`, so a later `add`, `remove`, or
562
+ * `clear` reaches the document registry without a second `publish`.
563
+ * - **One manager is followed at a time.** A `publish` naming another manager releases the
564
+ * subscription and takes up the new one, and `destroy` releases it outright. An event from a
565
+ * manager this handle no longer follows is ignored, which is what a listener republishing
566
+ * another manager from inside a dispatch produces. A followed change has no caller to refuse
567
+ * to, so a tool advertising neither a `description` nor a `summary` is left unregistered
568
+ * rather than refusing anything; `publish` still refuses such a batch whole. Under a name
569
+ * this handle never registered that skip emits no `change`, because nothing reached the
570
+ * document registry. A synchronisation registers only what it can carry, so such a tool
571
+ * standing under a name this handle already registered releases that registration rather
572
+ * than leaving it advertising a descriptor the manager no longer stands behind, and that
573
+ * release emits the registry's `change` like any other.
574
+ * - **A followed change is a trigger, not a fact.** Each `add`, `remove`, or `clear` queues one
575
+ * synchronisation of this handle's registrations for that manager against what the manager
576
+ * holds when that queued work runs. The event cannot decide the outcome: `remove` and
577
+ * `clear` name tools the manager no longer holds, an earlier listener in the same dispatch
578
+ * may already have put another tool under one of those names, and the manager's `destroy`
579
+ * empties its map after the `clear` it publishes. Reading the manager converges on its state
580
+ * however the change was reached, so a synchronisation queued and not yet started already
581
+ * covers every change that arrives before it runs and a second one is not queued. A
582
+ * publication queued behind that synchronisation ends its cover, because the publication
583
+ * prunes what the synchronisation registered: a change arriving after that call queues a
584
+ * synchronisation of its own, which runs after the publication.
585
+ * - **A later `publish` reconciles, and so does every synchronisation.** Each name is compared
586
+ * with what this handle already registered for it: the same manager holding the same tool
587
+ * leaves the registration alone, another tool of that same manager advertising an equal
588
+ * descriptor leaves it registered and records the tool it now stands for, and anything else
589
+ * releases it and registers the new descriptor bound to the new manager. A name the snapshot
590
+ * dropped, or a name the manager no longer holds, is released — after the batch has
591
+ * reconciled, never before, so no name is withdrawn while the tools replacing it are still
592
+ * being registered. A failed batch releases them too, and still withdraws nothing it
593
+ * carries. Nothing has to be removed from the manager to make the registry agree with it.
594
+ * - **Work serializes.** Registration is asynchronous and `destroy` is not, so overlapping
595
+ * calls would interleave registrations with the aborts meant to end them. Each publication
596
+ * and each synchronisation queues behind the previous one, and every step re-reads the
597
+ * destroyed flag, so a `destroy` issued mid-publish stops the registrations that have not
598
+ * happened yet instead of racing them.
599
+ * - **Nothing is polyfilled.** A document exposing no registry never reaches this class:
600
+ * {@link import('./factories.js').createModelContext} returns `undefined` instead, so feature
601
+ * absence stays absence rather than becoming a local implementation a caller mistakes for
602
+ * the platform.
603
+ * - **The result shape is the registry's.** An adopted tool resolves whatever `executeTool`
604
+ * resolved, unchanged. WebMCP's IDL types that `Promise<DOMString>` while the
605
+ * specification's README sample returns `{ content: [...] }`; the primary source disagrees
606
+ * with itself, and normalizing either way would encode a guess as a contract.
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * import { isWebMCPDocument, ModelContext } from '@orkestrel/mcp/browser'
611
+ *
612
+ * if (isWebMCPDocument(document)) {
613
+ * const bridge = new ModelContext(document)
614
+ * await bridge.publish(tools)
615
+ * }
616
+ * ```
617
+ */
618
+ export declare class ModelContext implements ModelContextInterface {
619
+ #private;
620
+ /**
621
+ * Binds a narrowed document's registry and arms the `toolchange` subscription.
622
+ *
623
+ * @param document - The document whose `modelContext` this handle bridges
624
+ * @param options - The emitter's initial hooks and listener-error handler; see
625
+ * {@link ModelContextOptions}
626
+ */
627
+ constructor(document: WebMCPDocument, options?: ModelContextOptions);
628
+ get emitter(): EmitterInterface<ModelContextEventMap>;
629
+ publish(tools: ToolManagerInterface, options?: ModelContextPublishOptions): Promise<void>;
630
+ adopt(options?: ModelContextAdoptOptions): Promise<readonly ToolInterface[]>;
631
+ destroy(): void;
632
+ }
633
+
634
+ /**
635
+ * Options for {@link ModelContextInterface.adopt} — the origins whose tools are read.
636
+ *
637
+ * @remarks
638
+ * `origins` is this package's one-word name for WebMCP's `fromOrigins`, forwarded unchanged.
639
+ * Omitting it reads this document's own registrations.
640
+ */
641
+ export declare interface ModelContextAdoptOptions {
642
+ readonly origins?: readonly string[];
643
+ }
644
+
645
+ /**
646
+ * Reports the moments a WebMCP registry's contents changed.
647
+ *
648
+ * @remarks
649
+ * Declared as a `type` alias rather than an interface, so the type-literal satisfies
650
+ * `EventMap` structurally. One event, because the registry publishes one: WebMCP's
651
+ * `toolchange` names no tool and carries no payload, so the bridge republishes it as a bare
652
+ * signal and a listener re-reads {@link ModelContextInterface.adopt} to learn what changed.
653
+ */
654
+ export declare type ModelContextEventMap = {
655
+ /** Reports that the document's registry changed — re-read it to learn how. */
656
+ readonly change: readonly [];
657
+ };
658
+
659
+ /**
660
+ * Represents the bridge between a {@link ToolManagerInterface} and a document's WebMCP
661
+ * registry — what {@link import('./factories.js').createModelContext} returns.
662
+ *
663
+ * @remarks
664
+ * Bidirectional and symmetric: `publish` sends this page's tools out to the registry, `adopt`
665
+ * brings the registry's tools back in as `@orkestrel/tool` `Tool` instances. Both directions
666
+ * run the same annotation projection in opposite directions.
667
+ *
668
+ * The handle aborts exactly the registrations it made. WebMCP registration identity is the
669
+ * tool name, per document, so a later registration of a name replaces the earlier one whichever
670
+ * handle made it, and this handle's release takes whatever now stands under the names it
671
+ * registered — including a same-name registration another handle made later. Names this handle
672
+ * never registered are untouched.
673
+ */
674
+ export declare interface ModelContextInterface {
675
+ /** Holds the emitter republishing the registry's `toolchange` as `change`. */
676
+ readonly emitter: EmitterInterface<ModelContextEventMap>;
677
+ /**
678
+ * Registers every tool the manager holds at this moment, then follows it.
679
+ *
680
+ * @remarks
681
+ * The snapshot is taken when the call is made, before the work queues behind an earlier
682
+ * `publish`, so a registry mutated while this call waits registers what it held at the call
683
+ * rather than what it holds when the queue reaches it.
684
+ *
685
+ * The same call subscribes to the manager's own `emitter`. Each `add`, `remove`, and
686
+ * `clear` it publishes queues one synchronisation of this handle's registrations against
687
+ * what the manager holds when that queued work runs, so the document registry converges on
688
+ * the tool registry after every change it follows, without a second call. Changes that
689
+ * arrive before that queued work runs are covered by it; a change that arrives after a
690
+ * later `publish` call takes a synchronisation of its own, which runs after that
691
+ * publication rather than before it. The event is the trigger and the manager is the fact:
692
+ * `remove` and `clear` carry tools the manager no longer holds, a listener that ran earlier
693
+ * in the same dispatch may already have put another tool under one of those names, and the
694
+ * manager's own `destroy` empties its map after publishing the `clear` that reports it. One
695
+ * manager is followed at a time: a `publish` naming another manager releases the
696
+ * subscription and takes up the new one, and `destroy` releases it outright. An event from
697
+ * a manager this handle no longer follows is ignored, which is what a listener republishing
698
+ * another manager from inside a dispatch produces. A followed change reaches no caller, so
699
+ * a tool advertising neither a `description` nor a `summary` is left unregistered rather
700
+ * than refusing anything, and a registration the document registry refuses is dropped, so
701
+ * the next call or the next synchronisation registers that name again. That batch still
702
+ * releases the names it dropped, because the prune runs whether or not every registration
703
+ * it asked for was made. A synchronisation registers only what it can carry, so such a tool
704
+ * standing under a name this handle already registered releases that registration instead
705
+ * of leaving it advertising a descriptor the manager no longer stands behind. Under a name
706
+ * this handle never registered the skip emits no `change`, because nothing reached the
707
+ * document registry; releasing a name this handle did register emits the registry's
708
+ * `change` like any other release. Compare the manager's own `definitions()` with what
709
+ * `adopt()` returns to read the mismatch, and
710
+ * {@link import('./helpers.js').describeWebMCPTool} answers `undefined` for a name
711
+ * `definitions()` still lists, which is that mismatch read from the manager's own side.
712
+ *
713
+ * Every name reconciles against what this handle registered for it, whether it arrives in a
714
+ * snapshot or through a synchronisation. The same manager still holding the same tool
715
+ * leaves the registration untouched, so a repeat publishes nothing and fires no
716
+ * `toolchange`. Another tool of that manager advertising an equal descriptor leaves the
717
+ * registration standing too, because execution routes through the manager by name and the
718
+ * replacement's handler is already what a foreign agent reaches. A changed projection, or
719
+ * the same name arriving from a different manager, releases the registration this handle
720
+ * holds and registers the new descriptor bound to the new manager. A name the snapshot
721
+ * dropped, and a name the followed manager no longer holds, is released after everything
722
+ * the batch carries has reconciled, so no name is withdrawn while the tools replacing it
723
+ * are still being registered.
724
+ *
725
+ * Every tool is projected before anything is registered, so a manager holding a tool with
726
+ * neither `description` nor `summary` is refused whole rather than half-registered.
727
+ *
728
+ * @param tools - The registry whose current tools are registered and whose later changes
729
+ * are followed
730
+ * @param options - The optional exposure list; see {@link ModelContextPublishOptions}
731
+ * @returns Resolves after the registry has accepted each added registration
732
+ * @throws Thrown when a tool carries neither a `description` nor a `summary`, because
733
+ * WebMCP requires the member and an empty string would be an invented one
734
+ */
735
+ publish(tools: ToolManagerInterface, options?: ModelContextPublishOptions): Promise<void>;
736
+ /**
737
+ * Reads the document's registered tools as locally executable tools.
738
+ *
739
+ * @remarks
740
+ * Each returned tool's `execute` runs the registry's `executeTool` and forwards its
741
+ * `ToolContext.signal` as WebMCP's `signal`, so an agent-side abort reaches the foreign
742
+ * tool. The value resolves unchanged: WebMCP's own sources disagree about whether a tool
743
+ * answers with a string or an MCP content record, so the bridge normalizes neither.
744
+ *
745
+ * An adopted tool advertises the foreign `inputSchema` as its `parameters` and validates
746
+ * nothing against it. Compiling that schema into a contract would let one page's
747
+ * unreadable or hostile schema refuse the whole `adopt` call, and the arguments reach a
748
+ * handler in another document that has to validate them anyway.
749
+ *
750
+ * @param options - The optional origin filter; see {@link ModelContextAdoptOptions}
751
+ * @returns The registry's tools, in registry order
752
+ */
753
+ adopt(options?: ModelContextAdoptOptions): Promise<readonly ToolInterface[]>;
754
+ /**
755
+ * Aborts every registration this handle made, stops following the tool registry, and
756
+ * releases the emitter — idempotent.
757
+ *
758
+ * @remarks
759
+ * The subscription is released before anything is aborted, and a synchronisation already
760
+ * queued reads the destroyed handle and stops, so no registration is made after this call
761
+ * and none is left behind for a later change to release.
762
+ *
763
+ * @returns Nothing
764
+ */
765
+ destroy(): void;
766
+ }
767
+
768
+ /**
769
+ * Options for {@link import('./factories.js').createModelContext} — the document to bridge
770
+ * and the emitter's initial wiring.
771
+ *
772
+ * @remarks
773
+ * - `document` — the document whose registry to bridge, defaulting to `globalThis.document`.
774
+ * A document exposing no `modelContext` makes the factory return `undefined`.
775
+ * - `on` — the reserved initial {@link import('@orkestrel/emitter').EmitterHooks} for
776
+ * {@link ModelContextEventMap}.
777
+ * - `error` — the emitter's listener-error handler; a listener throw routes here rather than
778
+ * to a domain event.
779
+ */
780
+ export declare interface ModelContextOptions {
781
+ readonly document?: Document;
782
+ readonly on?: EmitterHooks<ModelContextEventMap>;
783
+ readonly error?: EmitterErrorHandler;
784
+ }
785
+
786
+ /**
787
+ * Options for {@link ModelContextInterface.publish} — the origins each registration is
788
+ * exposed to.
789
+ *
790
+ * @remarks
791
+ * `origins` is this package's one-word name for WebMCP's `exposedTo`, forwarded unchanged.
792
+ * Omitting it registers with no exposure list, which is the registry's own default.
793
+ */
794
+ export declare interface ModelContextPublishOptions {
795
+ readonly origins?: readonly string[];
796
+ }
797
+
798
+ /**
799
+ * Represents one MCP server hosted inside the calling page — what
800
+ * {@link import('./factories.js').createPageServer} returns.
801
+ *
802
+ * @remarks
803
+ * The page twin of {@link ScopeServerInterface}, and it publishes the client beside the
804
+ * terminal because the pair's whole value is holding both ends. The returned `client` is
805
+ * bound but NOT connected: connection is a protocol round trip over the channel, so it stays
806
+ * the consumer's `await client.connect()` rather than a promise the factory hides.
807
+ *
808
+ * `stop` closes the client's port first, so the client observes the close and reports
809
+ * `connected` as `false`, then unbinds both sides and closes the server's port. It is
810
+ * idempotent, and it ends this handle's lifetime permanently — the same verb, with the same
811
+ * meaning, that {@link ScopeServerInterface} publishes for the same action.
812
+ *
813
+ * The published `client` stays reachable after `stop` and is inert: `call` and `tools` reject
814
+ * at once with an `MCPError` carrying `-32600`, because the pair's channel is closed and no
815
+ * `connect` can reopen it.
816
+ */
817
+ export declare interface PageServerInterface {
818
+ /** Holds the client bound to the server this page hosts, awaiting its own `connect`. */
819
+ readonly client: MCPClientInterface;
820
+ /** Closes both ports, unbinds both sides, and disconnects the client — idempotent. */
821
+ stop(): void;
822
+ }
823
+
824
+ /**
825
+ * Options for {@link import('./factories.js').createPageServer} — the live
826
+ * {@link ToolManagerInterface} to expose, the optional server identity, and the optional
827
+ * settings for the client half the pair owns.
828
+ *
829
+ * @remarks
830
+ * - `tools` — the registry the hosted `MCPServer` dispatches `tools/call` against. Required.
831
+ * - `name` / `version` — the hosted server's identity, defaulting to
832
+ * {@link import('./constants.js').DEFAULT_MCP_SERVER_NAME} /
833
+ * {@link import('./constants.js').DEFAULT_MCP_SERVER_VERSION} exactly as
834
+ * {@link ScopeServerOptions} does.
835
+ * - `client` — the client half's own settings, grouped because there is more than one, and
836
+ * declared as `MCPClientOptions` (`@orkestrel/mcp`) minus its one omission rather than as a
837
+ * restatement of its members: every member the client accepts reaches this factory, and a
838
+ * member added to that interface later reaches it without an edit here. `transport` is the
839
+ * omission, and the only one: the pair mints the `MessageChannel` and owns both ends of it,
840
+ * which is the whole point of the factory, so a caller-supplied carrier would be a second
841
+ * transport with nothing on the other side of it.
842
+ */
843
+ export declare interface PageServerOptions {
844
+ readonly tools: ToolManagerInterface;
845
+ readonly name?: string;
846
+ readonly version?: string;
847
+ readonly client?: Omit<MCPClientOptions, 'transport'>;
848
+ }
849
+
315
850
  /**
316
851
  * Describes the structural shape {@link import('./factories.js').createScopeServer} needs
317
852
  * from a
@@ -327,8 +862,11 @@ export declare interface MessagePortTransportOptions {
327
862
  * structurally (it exposes far more, which this narrower shape ignores).
328
863
  */
329
864
  export declare interface ScopeInterface {
865
+ /** Posts one reply onto the scope's implicit channel. */
330
866
  postMessage(message: unknown): void;
867
+ /** Subscribes to the scope's `message` events, portless and port-bearing alike. */
331
868
  addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
869
+ /** Drops a `message` subscription. */
332
870
  removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
333
871
  }
334
872
 
@@ -389,6 +927,254 @@ export declare interface ScopeTransportInterface extends MCPTransportInterface {
389
927
  deliver(message: string): void;
390
928
  }
391
929
 
930
+ /**
931
+ * Projects domain tool annotations onto WebMCP registry hints without inventing defaults.
932
+ *
933
+ * @param annotations - The authored domain annotations
934
+ * @returns The mapped hints; an omitted annotation stays omitted
935
+ *
936
+ * @example
937
+ * ```ts
938
+ * toolAnnotationsToWebMCP({ pure: true, untrusted: true }) // { readOnlyHint: true, untrustedContentHint: true }
939
+ * ```
940
+ */
941
+ export declare function toolAnnotationsToWebMCP(annotations: ToolAnnotations): WebMCPAnnotations;
942
+
943
+ /**
944
+ * Projects one advertised tool definition onto the WebMCP descriptor a registration carries.
945
+ *
946
+ * @remarks
947
+ * Takes the definition a `ToolManagerInterface` advertises rather than the tool itself, so the
948
+ * description here is the one the MCP wire advertises too — the registry substitutes an
949
+ * authored `summary` for the full `description`, and reading the same projection keeps one
950
+ * advertised description across both surfaces.
951
+ *
952
+ * WebMCP requires `description`, so a definition carrying none cannot be registered at all.
953
+ * Returning `undefined` is what lets the caller refuse the whole batch before registering any
954
+ * of it; registering an empty string instead would be an invented value a foreign agent reads
955
+ * as a real one.
956
+ *
957
+ * @param definition - The advertised definition to project
958
+ * @returns The WebMCP descriptor, or `undefined` when the definition advertises no description
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * toolToWebMCP({ name: 'add', description: 'Adds two numbers' })?.description // 'Adds two numbers'
963
+ * ```
964
+ */
965
+ export declare function toolToWebMCP(definition: ToolDefinition): WebMCPDescriptor | undefined;
966
+
967
+ /** Names the WebMCP registry event the bridge republishes as its own `change`. */
968
+ export declare const WEBMCP_CHANGE_EVENT = "toolchange";
969
+
970
+ /**
971
+ * Describes a tool's observable effects as the WebMCP registry declares them.
972
+ *
973
+ * @remarks
974
+ * Transliterates the WebMCP `ToolAnnotations` dictionary. The IDL defaults each member to
975
+ * `false`; this declaration keeps every member optional instead, because the bridge projects
976
+ * from `@orkestrel/tool`'s `ToolAnnotations` and never invents a hint the author omitted. The
977
+ * mapping is `pure` to `readOnlyHint`, `untrusted` to `untrustedContentHint`, and
978
+ * `consequential` to `consequentialHint` — a fuller correspondence than the MCP wire's, which
979
+ * has no counterpart for `untrusted` and spells the consequence `destructiveHint`.
980
+ */
981
+ export declare interface WebMCPAnnotations {
982
+ readonly readOnlyHint?: boolean;
983
+ readonly untrustedContentHint?: boolean;
984
+ readonly consequentialHint?: boolean;
985
+ }
986
+
987
+ /**
988
+ * Projects WebMCP registry hints onto domain tool annotations without inventing defaults.
989
+ *
990
+ * @param annotations - The registry's hints, as the WebMCP dictionary declares them
991
+ * @returns The mapped annotations; an omitted hint stays omitted
992
+ *
993
+ * @example
994
+ * ```ts
995
+ * webMCPAnnotationsToTool({ readOnlyHint: false, consequentialHint: true }) // { pure: false, consequential: true }
996
+ * ```
997
+ */
998
+ export declare function webMCPAnnotationsToTool(annotations: WebMCPAnnotations): ToolAnnotations;
999
+
1000
+ /**
1001
+ * Describes the members a WebMCP tool carries into the registry and back out of it.
1002
+ *
1003
+ * @remarks
1004
+ * The WebMCP `ModelContextTool` and `RegisteredTool` dictionaries declare the same members —
1005
+ * `name`, `title`, `description`, `inputSchema`, `annotations` — and differ only in what each
1006
+ * adds, so this is the shared body {@link WebMCPTool} and {@link WebMCPRegisteredTool} extend.
1007
+ * `description` is required in the IDL and stays required here, which is why `publish` refuses
1008
+ * a tool that authors neither a description nor a summary rather than registering an empty one.
1009
+ */
1010
+ export declare interface WebMCPDescriptor {
1011
+ readonly name: string;
1012
+ readonly title?: string;
1013
+ readonly description: string;
1014
+ readonly inputSchema?: Readonly<Record<string, unknown>>;
1015
+ readonly annotations?: WebMCPAnnotations;
1016
+ }
1017
+
1018
+ /**
1019
+ * Describes a document that exposes the WebMCP tool registry.
1020
+ *
1021
+ * @remarks
1022
+ * The narrowed shape {@link import('./validators.js').isWebMCPDocument} produces, and the one
1023
+ * value {@link import('./ModelContext.js').ModelContext} needs. It declares the `modelContext`
1024
+ * member alone rather than extending `Document`, because that member is the whole of what the
1025
+ * bridge dereferences — a real `Document` carrying the registry satisfies it structurally, and
1026
+ * so does any other host object that exposes one.
1027
+ */
1028
+ export declare interface WebMCPDocument {
1029
+ readonly modelContext: WebMCPRegistryInterface;
1030
+ }
1031
+
1032
+ /**
1033
+ * Runs one registered WebMCP tool with the caller's input and the registry's signal.
1034
+ *
1035
+ * @remarks
1036
+ * Transliterates the WebMCP `ToolExecuteCallback` callback. The IDL types its return
1037
+ * `Promise<any>` and this declaration narrows that to `Promise<unknown>`, because a caller
1038
+ * must narrow what a foreign tool returned before reading it.
1039
+ */
1040
+ export declare type WebMCPExecuteHandler = (input: Readonly<Record<string, unknown>>, options: WebMCPHandlerOptions) => Promise<unknown>;
1041
+
1042
+ /**
1043
+ * Options for the WebMCP registry's `executeTool` — the caller's cancellation signal.
1044
+ *
1045
+ * @remarks
1046
+ * Transliterates the WebMCP `ModelContextExecuteToolOptions` dictionary. The registry
1047
+ * forwards this signal onto the {@link WebMCPHandlerOptions} it hands the tool's callback.
1048
+ */
1049
+ export declare interface WebMCPExecuteOptions {
1050
+ readonly signal?: AbortSignal;
1051
+ }
1052
+
1053
+ /**
1054
+ * Carries the execution signal the WebMCP registry hands a registered tool's callback.
1055
+ *
1056
+ * @remarks
1057
+ * Transliterates the WebMCP `ToolExecuteCallbackOptions` dictionary, whose `signal` is
1058
+ * required: the registry always mints one, and an `executeTool` caller's own signal is
1059
+ * forwarded onto it.
1060
+ */
1061
+ export declare interface WebMCPHandlerOptions {
1062
+ readonly signal: AbortSignal;
1063
+ }
1064
+
1065
+ /**
1066
+ * Pairs one tool with the WebMCP descriptor a registry advertises for it.
1067
+ *
1068
+ * @remarks
1069
+ * The bridge's own pairing rather than a WebMCP dictionary. A registration has to answer later
1070
+ * whether the manager still holds the tool it was made for, and a descriptor alone cannot
1071
+ * answer that: two tools can advertise identical descriptors, and a descriptor JSON cannot
1072
+ * encode — a cyclic `inputSchema` — compares equal to none, its own included.
1073
+ */
1074
+ export declare interface WebMCPProjection {
1075
+ readonly tool: ToolInterface;
1076
+ readonly descriptor: WebMCPDescriptor;
1077
+ }
1078
+
1079
+ /**
1080
+ * Describes one tool as the WebMCP registry reports it back.
1081
+ *
1082
+ * @remarks
1083
+ * Transliterates the WebMCP `RegisteredTool` dictionary: {@link WebMCPDescriptor}'s members
1084
+ * plus the `window` that registered the tool and the `origin` it was registered from. The
1085
+ * bridge carries both unread — `executeTool` takes the whole record back — and reads the
1086
+ * descriptor members alone.
1087
+ */
1088
+ export declare interface WebMCPRegisteredTool extends WebMCPDescriptor {
1089
+ readonly window: Window;
1090
+ readonly origin: string;
1091
+ }
1092
+
1093
+ /**
1094
+ * Options for the WebMCP registry's `registerTool` — the exposure list and the
1095
+ * unregistration signal.
1096
+ *
1097
+ * @remarks
1098
+ * Transliterates the WebMCP `ModelContextRegisterToolOptions` dictionary. `exposedTo` lists
1099
+ * the origins the registration is visible to; `signal` is WebMCP's unregistration path —
1100
+ * aborting it removes the tool, which is why the bridge retains one controller per
1101
+ * registration.
1102
+ */
1103
+ export declare interface WebMCPRegisterOptions {
1104
+ readonly exposedTo?: readonly string[];
1105
+ readonly signal?: AbortSignal;
1106
+ }
1107
+
1108
+ /**
1109
+ * Represents the WebMCP tool registry a document exposes as `document.modelContext`.
1110
+ *
1111
+ * @remarks
1112
+ * Transliterates the WebMCP `ModelContext` interface, which extends `EventTarget`: the
1113
+ * operations plus the `toolchange` subscription the bridge republishes as
1114
+ * {@link ModelContextEventMap}'s `change`. Only the members the bridge touches are declared,
1115
+ * exactly as {@link ScopeInterface} declares only what `createScopeServer` touches, so a real
1116
+ * `ModelContext` satisfies this structurally and an IDL-faithful double satisfies it without
1117
+ * implementing the whole of `EventTarget`.
1118
+ *
1119
+ * `executeTool` resolves `unknown` because the primary source disagrees with itself: the IDL
1120
+ * types it `Promise<DOMString>` while the specification's own README sample returns the MCP
1121
+ * content shape `{ content: [...] }` from a tool's callback. The bridge records that
1122
+ * disagreement rather than resolving it, so it neither parses the answer as text nor projects
1123
+ * it into content blocks.
1124
+ */
1125
+ export declare interface WebMCPRegistryInterface {
1126
+ /** Registers one tool, resolving when the registry has accepted it. */
1127
+ registerTool(tool: WebMCPTool, options?: WebMCPRegisterOptions): Promise<void>;
1128
+ /** Reads the registered tools this document may see. */
1129
+ getTools(options?: WebMCPToolsOptions): Promise<readonly WebMCPRegisteredTool[]>;
1130
+ /** Runs one registered tool and resolves whatever its callback returned. */
1131
+ executeTool(tool: WebMCPRegisteredTool, input?: Readonly<Record<string, unknown>>, options?: WebMCPExecuteOptions): Promise<unknown>;
1132
+ /** Subscribes to the registry's `toolchange` event. */
1133
+ addEventListener(type: 'toolchange', listener: () => void): void;
1134
+ /** Drops a `toolchange` subscription. */
1135
+ removeEventListener(type: 'toolchange', listener: () => void): void;
1136
+ }
1137
+
1138
+ /**
1139
+ * Describes one tool as handed to the WebMCP registry for registration.
1140
+ *
1141
+ * @remarks
1142
+ * Transliterates the WebMCP `ModelContextTool` dictionary: {@link WebMCPDescriptor}'s members
1143
+ * plus the required `execute` callback the registry invokes.
1144
+ */
1145
+ export declare interface WebMCPTool extends WebMCPDescriptor {
1146
+ readonly execute: WebMCPExecuteHandler;
1147
+ }
1148
+
1149
+ /**
1150
+ * Options for the WebMCP registry's `getTools` — the origins whose tools are read.
1151
+ *
1152
+ * @remarks
1153
+ * Transliterates the WebMCP `ModelContextGetToolOptions` dictionary. Omitting `fromOrigins`
1154
+ * reads this document's own registrations.
1155
+ */
1156
+ export declare interface WebMCPToolsOptions {
1157
+ readonly fromOrigins?: readonly string[];
1158
+ }
1159
+
1160
+ /**
1161
+ * Projects one registered WebMCP tool onto the tool definition an adopted tool advertises.
1162
+ *
1163
+ * @remarks
1164
+ * The inverse of {@link toolToWebMCP}, and deliberately lossy in the other direction: the
1165
+ * registry's `window` and `origin` describe where the tool lives rather than what it does, and
1166
+ * the bridge hands the whole registered record back to `executeTool` instead of rebuilding it.
1167
+ *
1168
+ * @param registered - The registered tool the registry reported
1169
+ * @returns The definition an adopted tool advertises
1170
+ *
1171
+ * @example
1172
+ * ```ts
1173
+ * webMCPToTool({ name: 'add', description: 'Adds', window, origin: 'https://a.example' }).name // 'add'
1174
+ * ```
1175
+ */
1176
+ export declare function webMCPToTool(registered: WebMCPRegisteredTool): ToolDefinition;
1177
+
392
1178
  /**
393
1179
  * Drives a remote MCP server over the native `WebSocket` global from the browser face, as a
394
1180
  * client {@link MCPMessageTransportInterface}. This class is the browser sibling of the Node