@jarenjs/contract 0.83.3 → 0.85.0
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/README.md +45 -2
- package/dist/types/project/index.d.ts +1 -1
- package/dist/types/project/tools.d.ts +5 -4
- package/dist/types/webmcp.d.ts +54 -0
- package/docs/CONTRACT-FORMAT.md +1 -1
- package/package.json +9 -5
- package/src/project/index.js +1 -1
- package/src/project/tools.js +6 -5
- package/src/webmcp.js +193 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ task slot per operation and one registered effect; and the
|
|
|
21
21
|
consumer wants beside the runtime — a browser-safe public subset that is
|
|
22
22
|
itself a `$contract` document, a valid OpenAPI 3.1 document, TypeScript
|
|
23
23
|
declarations with a typed operation map, Markdown reference docs and
|
|
24
|
-
|
|
24
|
+
plain operation definitions — with a `jaren-contract` CLI whose
|
|
25
25
|
`--check` fails CI the moment an artifact drifts. The contract knows its
|
|
26
26
|
own identity: `contract.revision()` is the SHA-256 of the canonical
|
|
27
27
|
public projection, served at the well-known path and carried in every
|
|
@@ -513,7 +513,7 @@ toTypeScript(contract); // one .d.ts: CatalogLoadInput/Output per operatio
|
|
|
513
513
|
// typed Client and Handlers — invoke('product.save', …) is fully typed
|
|
514
514
|
toMarkdown(contract); // reference docs: operations table, per-operation sections, the type tables
|
|
515
515
|
contractTools(contract, client);
|
|
516
|
-
//
|
|
516
|
+
// plain ToolDefs for the shared WebMCP adapter:
|
|
517
517
|
// name 'product_save', a self-contained inputSchema, execute → the outcome
|
|
518
518
|
```
|
|
519
519
|
|
|
@@ -738,6 +738,7 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
738
738
|
| `@jarenjs/contract/package.json` | metadata | — |
|
|
739
739
|
| `@jarenjs/contract/provider` | JavaScript | declared |
|
|
740
740
|
| `@jarenjs/contract/command` | JavaScript | declared |
|
|
741
|
+
| `@jarenjs/contract/webmcp` | JavaScript | declared |
|
|
741
742
|
<!--/fact-->
|
|
742
743
|
|
|
743
744
|
Author JSON template catalogs and MessageSpec references with the
|
|
@@ -780,3 +781,45 @@ finalizers remain part of the same end-to-end test.
|
|
|
780
781
|
REST/GraphQL dialects and private run authority. Partial observations retain
|
|
781
782
|
wire text and never become complete snapshots. See
|
|
782
783
|
[provider descriptors and ingestion](docs/PROVIDER-FORMAT.md).
|
|
784
|
+
|
|
785
|
+
## Browser operation registration
|
|
786
|
+
|
|
787
|
+
`@jarenjs/contract/webmcp` exports `registerWebMcp` for plain invokable tool
|
|
788
|
+
definitions. Its browser boundary needs no assistant, provider or model. Supply
|
|
789
|
+
operations that already validate their inputs, for example projected contract
|
|
790
|
+
operations. Importing the module does not inspect browser globals.
|
|
791
|
+
|
|
792
|
+
```js
|
|
793
|
+
import { registerWebMcp } from '@jarenjs/contract/webmcp';
|
|
794
|
+
const binding = registerWebMcp(tools, { realm: window });
|
|
795
|
+
const outcome = await binding.ready;
|
|
796
|
+
// On host teardown:
|
|
797
|
+
await binding.dispose();
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
An explicitly supplied `context` is authoritative, including null or undefined.
|
|
801
|
+
Otherwise the adapter discovers a usable `document.modelContext`, then
|
|
802
|
+
`navigator.modelContext`, preserving method receivers and registering through
|
|
803
|
+
only one context. Missing/inaccessible objects and non-callable methods do not
|
|
804
|
+
mask the other root. `registerTool` takes precedence over legacy `provideContext`
|
|
805
|
+
on either root. Only the explicit `WEBMCP_UNSUPPORTED` sentinel permits fallback
|
|
806
|
+
after invoking a method; an injected implementation may return it only before
|
|
807
|
+
any mutation. Exceptions, permission refusals and partial registration never
|
|
808
|
+
trigger a second registration attempt.
|
|
809
|
+
|
|
810
|
+
The binding immediately exposes `ready`, `status` and `dispose`. `ready` resolves
|
|
811
|
+
to `registered`, `unavailable`, `failed` or `disposed` with counts, the selected
|
|
812
|
+
root/method and diagnostics; starting a promise does not report completion.
|
|
813
|
+
Browser failures retain their original error and separate cleanup errors.
|
|
814
|
+
Dispose deactivates callbacks immediately, then waits for pending registration
|
|
815
|
+
and removes only owned successful registrations. Repeated disposal is harmless.
|
|
816
|
+
|
|
817
|
+
Removal uses a returned cleanup handle or `unregisterTool` where available.
|
|
818
|
+
Registration always receives an abort signal. Set `lifecycle: 'abort'` only for
|
|
819
|
+
a host qualified to support it; otherwise native removal without a handle or
|
|
820
|
+
unregister method is explicitly `unverified`, while owned callbacks are inert.
|
|
821
|
+
Legacy `clearContext` is used only with `exclusiveLegacyContext: true`, a host
|
|
822
|
+
guarantee that this binding owns the whole catalog, and only for its current
|
|
823
|
+
owner. Shared legacy catalogs are never cleared. Later explicit mounts discover
|
|
824
|
+
capabilities again. Unknown future method shapes fail visibly without breaking
|
|
825
|
+
ordinary application operations.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `publicProjection` (the browser-safe subset — itself a `$contract`
|
|
4
4
|
* document, and what the revision hashes), `toOpenApi` (OpenAPI 3.1
|
|
5
5
|
* through a JSLT stylesheet), `toTypeScript` and `toMarkdown` (on
|
|
6
|
-
* `@jarenjs/emit`'s type model), `contractTools` (
|
|
6
|
+
* `@jarenjs/emit`'s type model), `contractTools` (plain operation
|
|
7
7
|
* definitions, no import edge) and the same-document bundler they share.
|
|
8
8
|
* This is the one subpath that imports `@jarenjs/emit` — the CLI loads
|
|
9
9
|
* it lazily, only for `types` and `docs`; a consumer that never imports
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file `contractTools`: the public, invokable operations of a contract as
|
|
3
|
-
* tool definitions for
|
|
3
|
+
* tool definitions for host operation registries — `{ name, description,
|
|
4
4
|
* inputSchema, execute }`, a plain object the toolbox and a WebMCP host
|
|
5
5
|
* read, so this package never imports the ai package (the generated-
|
|
6
6
|
* document rule). `execute` calls the client's `invoke` and answers the
|
|
@@ -50,8 +50,9 @@ export type ContractToolsOptions = {
|
|
|
50
50
|
* without `invoke`, `ops` naming an unknown or opaque operation, a tool
|
|
51
51
|
* name outside `^[a-zA-Z0-9_-]{1,64}$`, or two operations mapping to one name
|
|
52
52
|
* @example
|
|
53
|
-
* const
|
|
54
|
-
*
|
|
55
|
-
* await
|
|
53
|
+
* const tools = contractTools(contract, client);
|
|
54
|
+
* const save = tools.find(tool => tool.name === 'product_save');
|
|
55
|
+
* await save.execute({ id: 1, revision: 2, product: { title: 'Example' } }); // → an outcome
|
|
56
|
+
* // registerWebMcp(tools) exposes the same definitions to a browser host.
|
|
56
57
|
*/
|
|
57
58
|
export declare function contractTools(contract: Contract, client: ToolClient, options?: ContractToolsOptions): ToolDefinition[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Browser registration of plain operations, with an owned asynchronous lifecycle. */
|
|
2
|
+
/** An injected adapter may return this only when an operation had no side effects. */
|
|
3
|
+
export declare const WEBMCP_UNSUPPORTED: unique symbol;
|
|
4
|
+
export type WebMcpTool = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: object;
|
|
8
|
+
execute: (input: any, options?: {
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}) => any;
|
|
11
|
+
annotations?: Record<string, boolean>;
|
|
12
|
+
};
|
|
13
|
+
export type WebMcpDiagnostic = {
|
|
14
|
+
code: string;
|
|
15
|
+
root?: string;
|
|
16
|
+
error?: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type WebMcpResult = {
|
|
19
|
+
status: 'pending' | 'registered' | 'unavailable' | 'failed' | 'disposed';
|
|
20
|
+
registered: number;
|
|
21
|
+
root: string | null;
|
|
22
|
+
method: string | null;
|
|
23
|
+
error?: unknown;
|
|
24
|
+
diagnostics: WebMcpDiagnostic[];
|
|
25
|
+
cleanupErrors: unknown[];
|
|
26
|
+
nativeRemoval: 'none' | 'unregister' | 'abort' | 'handle' | 'legacy-clear' | 'unverified';
|
|
27
|
+
};
|
|
28
|
+
export type WebMcpOptions = {
|
|
29
|
+
context?: unknown;
|
|
30
|
+
realm?: object;
|
|
31
|
+
onError?: (error: unknown) => void;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
lifecycle?: 'abort' | 'callback-only';
|
|
34
|
+
exclusiveLegacyContext?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Register plain, already validated operations. Explicit context overrides are
|
|
38
|
+
* authoritative. Otherwise a usable document context precedes navigator.
|
|
39
|
+
* `ready` resolves after completion; dispose deactivates callbacks immediately
|
|
40
|
+
* and waits for pending registration and owned native cleanup. Neither promise
|
|
41
|
+
* rejects on browser failures. Each explicit call discovers capabilities again.
|
|
42
|
+
*
|
|
43
|
+
* `lifecycle: 'abort'` qualifies a host known to support registration signals.
|
|
44
|
+
* Without an unregister method, cleanup handle or that qualification, callbacks
|
|
45
|
+
* are deactivated and native removal is reported as unverified. A legacy clear
|
|
46
|
+
* requires the host's explicit exclusive-catalog guarantee.
|
|
47
|
+
* @param {WebMcpTool[]} definitions
|
|
48
|
+
* @param {WebMcpOptions} [options]
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerWebMcp(definitions: WebMcpTool[], options?: WebMcpOptions): {
|
|
51
|
+
ready: Promise<WebMcpResult>;
|
|
52
|
+
dispose: () => Promise<WebMcpResult>;
|
|
53
|
+
readonly status: "disposed" | "failed" | "pending" | "registered" | "unavailable";
|
|
54
|
+
};
|
package/docs/CONTRACT-FORMAT.md
CHANGED
|
@@ -1940,7 +1940,7 @@ rendered by emit's Markdown target over the **same** type model as
|
|
|
1940
1940
|
|
|
1941
1941
|
`contractTools(contract, client, { ops?, name? })` → an array of
|
|
1942
1942
|
`{ name, description, inputSchema, execute }` — the `ToolDef` shape
|
|
1943
|
-
|
|
1943
|
+
browser or host operation registries takes and WebMCP's `registerTool`
|
|
1944
1944
|
reads, **without importing that package** (the generated-document rule:
|
|
1945
1945
|
the shape is a plain object; the test suite registers them into a real
|
|
1946
1946
|
toolbox). Per public, invokable operation (opaque and subscribe
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/contract",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.85.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"./command": {
|
|
68
68
|
"types": "./dist/types/command.d.ts",
|
|
69
69
|
"default": "./src/command.js"
|
|
70
|
+
},
|
|
71
|
+
"./webmcp": {
|
|
72
|
+
"types": "./dist/types/webmcp.d.ts",
|
|
73
|
+
"default": "./src/webmcp.js"
|
|
70
74
|
}
|
|
71
75
|
},
|
|
72
76
|
"files": [
|
|
@@ -110,9 +114,9 @@
|
|
|
110
114
|
"prepack": "npm run build:types"
|
|
111
115
|
},
|
|
112
116
|
"dependencies": {
|
|
113
|
-
"@jarenjs/core": "^0.
|
|
114
|
-
"@jarenjs/json": "^0.
|
|
115
|
-
"@jarenjs/validate": "^0.
|
|
116
|
-
"@jarenjs/emit": "^0.
|
|
117
|
+
"@jarenjs/core": "^0.85.0",
|
|
118
|
+
"@jarenjs/json": "^0.85.0",
|
|
119
|
+
"@jarenjs/validate": "^0.85.0",
|
|
120
|
+
"@jarenjs/emit": "^0.85.0"
|
|
117
121
|
}
|
|
118
122
|
}
|
package/src/project/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `publicProjection` (the browser-safe subset — itself a `$contract`
|
|
5
5
|
* document, and what the revision hashes), `toOpenApi` (OpenAPI 3.1
|
|
6
6
|
* through a JSLT stylesheet), `toTypeScript` and `toMarkdown` (on
|
|
7
|
-
* `@jarenjs/emit`'s type model), `contractTools` (
|
|
7
|
+
* `@jarenjs/emit`'s type model), `contractTools` (plain operation
|
|
8
8
|
* definitions, no import edge) and the same-document bundler they share.
|
|
9
9
|
* This is the one subpath that imports `@jarenjs/emit` — the CLI loads
|
|
10
10
|
* it lazily, only for `types` and `docs`; a consumer that never imports
|
package/src/project/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
/**
|
|
3
3
|
* @file `contractTools`: the public, invokable operations of a contract as
|
|
4
|
-
* tool definitions for
|
|
4
|
+
* tool definitions for host operation registries — `{ name, description,
|
|
5
5
|
* inputSchema, execute }`, a plain object the toolbox and a WebMCP host
|
|
6
6
|
* read, so this package never imports the ai package (the generated-
|
|
7
7
|
* document rule). `execute` calls the client's `invoke` and answers the
|
|
@@ -23,7 +23,7 @@ import { bundleSameDocument } from '../bundle.js';
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* A tool definition as
|
|
26
|
+
* A tool definition as browser or host operation registries takes it —
|
|
27
27
|
* the same shape WebMCP's `registerTool` reads.
|
|
28
28
|
* @typedef {Object} ToolDefinition
|
|
29
29
|
* @property {string} name
|
|
@@ -92,9 +92,10 @@ function toolInputSchema(op, contract) {
|
|
|
92
92
|
* without `invoke`, `ops` naming an unknown or opaque operation, a tool
|
|
93
93
|
* name outside `^[a-zA-Z0-9_-]{1,64}$`, or two operations mapping to one name
|
|
94
94
|
* @example
|
|
95
|
-
* const
|
|
96
|
-
*
|
|
97
|
-
* await
|
|
95
|
+
* const tools = contractTools(contract, client);
|
|
96
|
+
* const save = tools.find(tool => tool.name === 'product_save');
|
|
97
|
+
* await save.execute({ id: 1, revision: 2, product: { title: 'Example' } }); // → an outcome
|
|
98
|
+
* // registerWebMcp(tools) exposes the same definitions to a browser host.
|
|
98
99
|
*/
|
|
99
100
|
export function contractTools(contract, client, options = {}) {
|
|
100
101
|
if (!isJsonObject(options)) throw new ContractHostError('JC1008', 'contractTools: options must be an object');
|
package/src/webmcp.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Browser registration of plain operations, with an owned asynchronous lifecycle. */
|
|
3
|
+
|
|
4
|
+
/** An injected adapter may return this only when an operation had no side effects. */
|
|
5
|
+
export const WEBMCP_UNSUPPORTED = Symbol('WebMCP operation unsupported before registration');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ name: string, description: string, inputSchema: object,
|
|
9
|
+
* execute: (input: any, options?: { signal?: AbortSignal }) => any,
|
|
10
|
+
* annotations?: Record<string, boolean> }} WebMcpTool
|
|
11
|
+
* @typedef {{ code: string, root?: string, error?: unknown }} WebMcpDiagnostic
|
|
12
|
+
* @typedef {{ status: 'pending'|'registered'|'unavailable'|'failed'|'disposed',
|
|
13
|
+
* registered: number, root: string|null, method: string|null,
|
|
14
|
+
* error?: unknown, diagnostics: WebMcpDiagnostic[], cleanupErrors: unknown[],
|
|
15
|
+
* nativeRemoval: 'none'|'unregister'|'abort'|'handle'|'legacy-clear'|'unverified' }} WebMcpResult
|
|
16
|
+
* @typedef {{ context?: unknown, realm?: object,
|
|
17
|
+
* onError?: (error: unknown) => void, signal?: AbortSignal,
|
|
18
|
+
* lifecycle?: 'abort'|'callback-only', exclusiveLegacyContext?: boolean }} WebMcpOptions
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Only an explicitly exclusive legacy binding may clear its whole catalog. */
|
|
22
|
+
const legacyOwners = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register plain, already validated operations. Explicit context overrides are
|
|
26
|
+
* authoritative. Otherwise a usable document context precedes navigator.
|
|
27
|
+
* `ready` resolves after completion; dispose deactivates callbacks immediately
|
|
28
|
+
* and waits for pending registration and owned native cleanup. Neither promise
|
|
29
|
+
* rejects on browser failures. Each explicit call discovers capabilities again.
|
|
30
|
+
*
|
|
31
|
+
* `lifecycle: 'abort'` qualifies a host known to support registration signals.
|
|
32
|
+
* Without an unregister method, cleanup handle or that qualification, callbacks
|
|
33
|
+
* are deactivated and native removal is reported as unverified. A legacy clear
|
|
34
|
+
* requires the host's explicit exclusive-catalog guarantee.
|
|
35
|
+
* @param {WebMcpTool[]} definitions
|
|
36
|
+
* @param {WebMcpOptions} [options]
|
|
37
|
+
*/
|
|
38
|
+
export function registerWebMcp(definitions, options = {}) {
|
|
39
|
+
const names = new Set();
|
|
40
|
+
for (const tool of definitions) {
|
|
41
|
+
if (!tool || typeof tool.name !== 'string' || !tool.name || names.has(tool.name)
|
|
42
|
+
|| typeof tool.description !== 'string' || typeof tool.execute !== 'function'
|
|
43
|
+
|| !tool.inputSchema || typeof tool.inputSchema !== 'object')
|
|
44
|
+
throw new TypeError('WebMCP needs uniquely named, invokable tool definitions');
|
|
45
|
+
names.add(tool.name);
|
|
46
|
+
}
|
|
47
|
+
/** @type {WebMcpResult} */
|
|
48
|
+
const result = { status: 'pending', registered: 0, root: null, method: null,
|
|
49
|
+
diagnostics: [], cleanupErrors: [], nativeRemoval: 'none' };
|
|
50
|
+
let active = true, disposed = false;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
/** @type {Promise<WebMcpResult>|undefined} */
|
|
53
|
+
let disposal;
|
|
54
|
+
/** @type {Array<() => unknown>} */
|
|
55
|
+
const removals = [];
|
|
56
|
+
const token = {};
|
|
57
|
+
/** @type {object|undefined} */
|
|
58
|
+
let selected;
|
|
59
|
+
/** @param {unknown} error */
|
|
60
|
+
function report(error) {
|
|
61
|
+
try { options.onError?.(error); }
|
|
62
|
+
catch (hookError) { result.diagnostics.push({ code: 'error-handler-failed', error: hookError }); }
|
|
63
|
+
}
|
|
64
|
+
/** Read accessors inside the failure boundary, including method accessors. */
|
|
65
|
+
function member(object, key, root) {
|
|
66
|
+
try { return object?.[key]; }
|
|
67
|
+
catch (error) { result.diagnostics.push({ code: 'inaccessible', root, error }); return undefined; }
|
|
68
|
+
}
|
|
69
|
+
function candidates() {
|
|
70
|
+
if (Object.hasOwn(options, 'context')) return [{ value: options.context, root: 'explicit' }];
|
|
71
|
+
const realm = options.realm ?? globalThis;
|
|
72
|
+
return ['document', 'navigator'].map(root => ({
|
|
73
|
+
root, value: member(member(realm, root, root), 'modelContext', root),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/** @type {WebMcpTool[]} */
|
|
77
|
+
const tools = definitions.map(tool => ({ ...tool, execute(input, execution) {
|
|
78
|
+
if (!active) return { error: 'WebMCP registration is inactive' };
|
|
79
|
+
return tool.execute(input, execution);
|
|
80
|
+
} }));
|
|
81
|
+
async function cleanup() {
|
|
82
|
+
active = false;
|
|
83
|
+
controller.abort();
|
|
84
|
+
for (const remove of removals.splice(0).reverse()) {
|
|
85
|
+
try { await remove(); }
|
|
86
|
+
catch (error) { result.cleanupErrors.push(error); report(error); }
|
|
87
|
+
}
|
|
88
|
+
if (selected && legacyOwners.get(selected) === token) legacyOwners.delete(selected);
|
|
89
|
+
}
|
|
90
|
+
function removal(context, method, name, handle, root) {
|
|
91
|
+
if (typeof handle === 'function') {
|
|
92
|
+
result.nativeRemoval = 'handle';
|
|
93
|
+
removals.push(handle);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const handleDispose = member(handle, 'dispose', root);
|
|
97
|
+
if (typeof handleDispose === 'function') {
|
|
98
|
+
result.nativeRemoval = 'handle';
|
|
99
|
+
removals.push(() => handleDispose.call(handle));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const unregister = member(context, 'unregisterTool', root);
|
|
103
|
+
if (method === 'registerTool' && typeof unregister === 'function') {
|
|
104
|
+
result.nativeRemoval = 'unregister';
|
|
105
|
+
removals.push(() => unregister.call(context, name));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const clear = member(context, 'clearContext', root);
|
|
109
|
+
if (method === 'provideContext' && options.exclusiveLegacyContext && typeof clear === 'function') {
|
|
110
|
+
result.nativeRemoval = 'legacy-clear';
|
|
111
|
+
removals.push(() => legacyOwners.get(context) === token ? clear.call(context) : undefined);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
result.nativeRemoval = options.lifecycle === 'abort' ? 'abort' : 'unverified';
|
|
115
|
+
if (result.nativeRemoval === 'unverified' && !result.diagnostics.some(d => d.code === 'native-removal-unverified'))
|
|
116
|
+
result.diagnostics.push({ code: 'native-removal-unverified', root });
|
|
117
|
+
}
|
|
118
|
+
async function register() {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
for (const { value: context, root } of candidates()) {
|
|
121
|
+
if (disposed) break;
|
|
122
|
+
if (context == null || (typeof context !== 'object' && typeof context !== 'function')) {
|
|
123
|
+
result.diagnostics.push({ code: context == null ? 'absent' : 'incompatible', root });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(context)) continue;
|
|
127
|
+
seen.add(context);
|
|
128
|
+
let usable = false;
|
|
129
|
+
for (const method of ['registerTool', 'provideContext']) {
|
|
130
|
+
const invoke = member(context, method, root);
|
|
131
|
+
if (typeof invoke !== 'function') continue;
|
|
132
|
+
usable = true;
|
|
133
|
+
selected = context;
|
|
134
|
+
result.root = root;
|
|
135
|
+
result.method = method;
|
|
136
|
+
const batch = method === 'registerTool' ? tools : [{ tools }];
|
|
137
|
+
let unsupported = false;
|
|
138
|
+
try {
|
|
139
|
+
for (const item of batch) {
|
|
140
|
+
if (disposed) break;
|
|
141
|
+
const handle = await invoke.call(context, item, { signal: controller.signal });
|
|
142
|
+
if (handle === WEBMCP_UNSUPPORTED) {
|
|
143
|
+
if (result.registered) throw new Error('WebMCP became unsupported after partial registration');
|
|
144
|
+
unsupported = true;
|
|
145
|
+
result.diagnostics.push({ code: 'unsupported-before-registration', root });
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
result.registered += method === 'registerTool' ? 1 : tools.length;
|
|
149
|
+
if (method === 'provideContext') legacyOwners.set(context, token);
|
|
150
|
+
removal(context, method, 'name' in item ? item.name : undefined, handle, root);
|
|
151
|
+
}
|
|
152
|
+
if (unsupported) continue;
|
|
153
|
+
result.status = disposed ? 'disposed' : 'registered';
|
|
154
|
+
if (disposed) await cleanup();
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
result.error = error;
|
|
159
|
+
result.status = disposed ? 'disposed' : 'failed';
|
|
160
|
+
report(error);
|
|
161
|
+
await cleanup();
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!usable) result.diagnostics.push({ code: 'incompatible', root });
|
|
166
|
+
}
|
|
167
|
+
active = false;
|
|
168
|
+
result.status = disposed ? 'disposed' : 'unavailable';
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/** Stop callbacks now, including while the browser is still registering. */
|
|
172
|
+
function dispose() {
|
|
173
|
+
if (disposal) return disposal;
|
|
174
|
+
disposed = true;
|
|
175
|
+
active = false;
|
|
176
|
+
controller.abort();
|
|
177
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
178
|
+
disposal = ready.then(async () => { await cleanup(); result.status = 'disposed'; return result; });
|
|
179
|
+
return disposal;
|
|
180
|
+
}
|
|
181
|
+
function onAbort() { void dispose(); }
|
|
182
|
+
/** @type {(value: WebMcpResult) => void} */
|
|
183
|
+
let complete;
|
|
184
|
+
/** @type {Promise<WebMcpResult>} */
|
|
185
|
+
const ready = new Promise(resolve => { complete = resolve; });
|
|
186
|
+
if (options.signal?.aborted) { disposed = true; active = false; controller.abort(); }
|
|
187
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
188
|
+
void register().then(complete, error => {
|
|
189
|
+
result.error = error; result.status = 'failed'; active = false; report(error);
|
|
190
|
+
complete(result);
|
|
191
|
+
});
|
|
192
|
+
return { ready, dispose, get status() { return result.status; } };
|
|
193
|
+
}
|