@frontmcp/plugin-webmcp 1.9.3 → 1.9.5
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 +63 -8
- package/esm/index.mjs +209 -8
- package/esm/package.json +15 -5
- package/esm/register.mjs +153 -0
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +209 -8
- package/package.json +15 -5
- package/register.d.ts +4 -0
- package/register.d.ts.map +1 -0
- package/register.js +176 -0
- package/webmcp.bridge.d.ts +4 -7
- package/webmcp.bridge.d.ts.map +1 -1
- package/webmcp.handoff.d.ts +10 -0
- package/webmcp.handoff.d.ts.map +1 -0
- package/webmcp.lazy.d.ts +50 -0
- package/webmcp.lazy.d.ts.map +1 -0
- package/webmcp.model-context.d.ts +6 -0
- package/webmcp.model-context.d.ts.map +1 -0
- package/webmcp.options.d.ts +7 -0
- package/webmcp.options.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -38,13 +38,14 @@ An agent's call runs the server's `tools:call-tool` flow, so hooks, authorities,
|
|
|
38
38
|
|
|
39
39
|
## Options
|
|
40
40
|
|
|
41
|
-
| Option | Type | Default | Description
|
|
42
|
-
| -------------- | ---------------------------------------------- | ------------------------- |
|
|
43
|
-
| `prefix` | `string` | `''` | Prepended to every exposed tool name, e.g. `'shop.'`.
|
|
44
|
-
| `include` | `(tool) => boolean` | all | Decides which listed tools are exposed.
|
|
45
|
-
| `exposedTo` | `string[]` | — | Other origins (e.g. an iframe's parent) the tools are offered to.
|
|
46
|
-
| `authContext` | `DirectAuthContext \| () => DirectAuthContext` | anonymous `webmcp` caller | Who the server sees calling. A function is resolved on every call.
|
|
47
|
-
| `modelContext` | `ModelContext` | `document.modelContext` | The context to register with: a polyfill, or a test double.
|
|
41
|
+
| Option | Type | Default | Description |
|
|
42
|
+
| -------------- | ---------------------------------------------- | ------------------------- | ------------------------------------------------------------------- |
|
|
43
|
+
| `prefix` | `string` | `''` | Prepended to every exposed tool name, e.g. `'shop.'`. |
|
|
44
|
+
| `include` | `(tool) => boolean` | all | Decides which listed tools are exposed. |
|
|
45
|
+
| `exposedTo` | `string[]` | — | Other origins (e.g. an iframe's parent) the tools are offered to. |
|
|
46
|
+
| `authContext` | `DirectAuthContext \| () => DirectAuthContext` | anonymous `webmcp` caller | Who the server sees calling. A function is resolved on every call. |
|
|
47
|
+
| `modelContext` | `ModelContext` | `document.modelContext` | The context to register with: a polyfill, or a test double. |
|
|
48
|
+
| `result` | `'structured' \| 'content' \| 'both'` | `'structured'` | What a call resolves to (see [Results](#how-tools-are-translated)). |
|
|
48
49
|
|
|
49
50
|
## Choosing which tools agents see
|
|
50
51
|
|
|
@@ -61,9 +62,63 @@ Tools are listed through the `'webmcp'` call surface, so `availableWhen` decides
|
|
|
61
62
|
`app:tool` becomes `app_tool`), names are cut to 128 characters, and collisions get `_2`, `_3`, …
|
|
62
63
|
- **Annotations.** MCP's `readOnlyHint` maps to `readOnlyHint`. An explicit `destructiveHint: true`
|
|
63
64
|
maps to `consequentialHint`, and an explicit `openWorldHint: true` to `untrustedContentHint`.
|
|
64
|
-
- **Results.**
|
|
65
|
+
- **Results.** An agent reads the whole result as text, so by default it gets each result once. With
|
|
66
|
+
`result: 'structured'` (the default) a call resolves to the tool's `structuredContent` alone when its content only
|
|
67
|
+
repeats it: one text block with the same JSON, or the primitive a `{ content }` wrapper holds, as the server writes
|
|
68
|
+
it for clients that don't read structured output. Otherwise it resolves to `{ content }`, plus `structuredContent`
|
|
69
|
+
when there is one, so text that says more and images are kept. `'content'` always resolves to `{ content }`, and
|
|
70
|
+
`'both'` to `{ content, structuredContent }` as an MCP client gets them (the shape up to 1.9.4). `_meta` is never
|
|
71
|
+
included.
|
|
65
72
|
- **Errors.** An error result (`isError`), or an error from the server, rejects with its message.
|
|
66
73
|
|
|
74
|
+
## Registering tools before the server loads
|
|
75
|
+
|
|
76
|
+
A server in the page is a large bundle, and starting it is one long task. To keep it off page load, compute the
|
|
77
|
+
tool list at build time and register it in the page; the server loads on an agent's first call:
|
|
78
|
+
|
|
79
|
+
`server.ts`, one factory for the build script and the page:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
export function createShopServer({ modelContext }: { modelContext?: ModelContext } = {}) {
|
|
83
|
+
return create({
|
|
84
|
+
info: { name: 'shop', version: '1.0.0' },
|
|
85
|
+
tools: [SearchProducts, AddToCart],
|
|
86
|
+
plugins: [WebMcpPlugin.init({ prefix: 'shop.', modelContext })],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The build script (Node):
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { listWebMcpTools } from '@frontmcp/plugin-webmcp';
|
|
95
|
+
|
|
96
|
+
const tools = await listWebMcpTools((modelContext) => createShopServer({ modelContext }));
|
|
97
|
+
await writeFile('src/webmcp-tools.json', JSON.stringify(tools));
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The page entry, a chunk without the SDK:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { registerWebMcpTools, resolveDocumentModelContext } from '@frontmcp/plugin-webmcp/register';
|
|
104
|
+
|
|
105
|
+
import tools from './webmcp-tools.json';
|
|
106
|
+
|
|
107
|
+
void registerWebMcpTools(resolveDocumentModelContext(), tools, (modelContext) =>
|
|
108
|
+
import('./server').then(({ createShopServer }) => createShopServer({ modelContext })),
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `listWebMcpTools(factory)` builds the server with a `modelContext` that records what the plugin registers,
|
|
113
|
+
returns those descriptors (name, title, description, input schema, hints) and disposes the server. The factory
|
|
114
|
+
must pass the `modelContext` it is given to `WebMcpPlugin.init()`.
|
|
115
|
+
- `registerWebMcpTools(modelContext, tools, loadServer, { exposedTo? })` registers the list at once. The first call
|
|
116
|
+
loads the server with `loadServer` (once, however many calls arrive together) and runs through the plugin; a failed
|
|
117
|
+
load rejects that call (a server it built is disposed, and the listed tools stay), and the next call tries again. From then on the plugin registers and unregisters tools as
|
|
118
|
+
the server's tools change, and `server.dispose()` unregisters them. A listed tool the loaded server lacks is
|
|
119
|
+
unregistered. Without a `modelContext` (no WebMCP) it does nothing.
|
|
120
|
+
- `@frontmcp/plugin-webmcp/register` does not import `@frontmcp/sdk`; it is a few kilobytes.
|
|
121
|
+
|
|
67
122
|
## Browser support
|
|
68
123
|
|
|
69
124
|
WebMCP is in origin trial in Chrome and Edge (Chrome 149+). For local development, enable
|
package/esm/index.mjs
CHANGED
|
@@ -18,9 +18,17 @@ import {
|
|
|
18
18
|
InternalMcpError
|
|
19
19
|
} from "@frontmcp/sdk";
|
|
20
20
|
import { randomUUID, runRequestExclusive } from "@frontmcp/utils";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var
|
|
21
|
+
|
|
22
|
+
// plugins/plugin-webmcp/src/webmcp.handoff.ts
|
|
23
|
+
var SYNC_LISTENER = /* @__PURE__ */ Symbol.for("@frontmcp/plugin-webmcp/sync-listener");
|
|
24
|
+
function withSyncListener(modelContext, listener) {
|
|
25
|
+
return Object.assign(modelContext, { [SYNC_LISTENER]: listener });
|
|
26
|
+
}
|
|
27
|
+
function syncListenerOf(modelContext) {
|
|
28
|
+
return modelContext?.[SYNC_LISTENER];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// plugins/plugin-webmcp/src/webmcp.model-context.ts
|
|
24
32
|
function resolveDocumentModelContext() {
|
|
25
33
|
const modelContext = globalThis.document?.modelContext;
|
|
26
34
|
return typeof modelContext === "object" && modelContext !== null && typeof modelContext.registerTool === "function" ? modelContext : void 0;
|
|
@@ -28,6 +36,11 @@ function resolveDocumentModelContext() {
|
|
|
28
36
|
function isWebMcpSupported() {
|
|
29
37
|
return resolveDocumentModelContext() !== void 0;
|
|
30
38
|
}
|
|
39
|
+
|
|
40
|
+
// plugins/plugin-webmcp/src/webmcp.bridge.ts
|
|
41
|
+
var MAX_WEBMCP_NAME_LENGTH = 128;
|
|
42
|
+
var INVALID_NAME_CHARACTERS = /[^A-Za-z0-9_.-]/g;
|
|
43
|
+
var ANONYMOUS_SUBJECT = "anon:webmcp";
|
|
31
44
|
function toWebMcpToolName(name) {
|
|
32
45
|
const cleaned = name.replace(INVALID_NAME_CHARACTERS, "_").slice(0, MAX_WEBMCP_NAME_LENGTH);
|
|
33
46
|
return cleaned.length > 0 ? cleaned : "_";
|
|
@@ -47,6 +60,27 @@ function toWebMcpAnnotations(annotations) {
|
|
|
47
60
|
if (annotations?.openWorldHint === true) hints.untrustedContentHint = true;
|
|
48
61
|
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
49
62
|
}
|
|
63
|
+
function isSameJson(text, value) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(JSON.parse(text)) === JSON.stringify(value);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function isTextCopyOf(content, structuredContent) {
|
|
71
|
+
const [block, ...others] = content;
|
|
72
|
+
if (block?.type !== "text" || others.length > 0) return false;
|
|
73
|
+
if (isSameJson(block.text, structuredContent)) return true;
|
|
74
|
+
const keys = Object.keys(structuredContent);
|
|
75
|
+
return keys.length === 1 && keys[0] === "content" && block.text === String(structuredContent["content"] ?? "");
|
|
76
|
+
}
|
|
77
|
+
function toAgentResult(result, mode) {
|
|
78
|
+
const content = result.content ?? [];
|
|
79
|
+
const structuredContent = result.structuredContent;
|
|
80
|
+
if (mode === "content" || structuredContent === void 0) return { content };
|
|
81
|
+
if (mode === "structured" && isTextCopyOf(content, structuredContent)) return structuredContent;
|
|
82
|
+
return { content, structuredContent };
|
|
83
|
+
}
|
|
50
84
|
function textOf(content) {
|
|
51
85
|
return (content ?? []).map((block) => block.type === "text" ? block.text : "").filter((text) => text.length > 0).join("\n");
|
|
52
86
|
}
|
|
@@ -103,6 +137,7 @@ var WebMcpBridge = class {
|
|
|
103
137
|
return;
|
|
104
138
|
}
|
|
105
139
|
this.modelContext = modelContext;
|
|
140
|
+
syncListenerOf(modelContext)?.started();
|
|
106
141
|
this.unsubscribe = this.scope.tools.subscribe({ immediate: true }, () => this.refresh());
|
|
107
142
|
}
|
|
108
143
|
/**
|
|
@@ -131,15 +166,19 @@ var WebMcpBridge = class {
|
|
|
131
166
|
/** Sync until no change is pending. Starts a microtask later, so a burst of changes syncs once. */
|
|
132
167
|
async drain() {
|
|
133
168
|
await Promise.resolve();
|
|
169
|
+
let failure;
|
|
134
170
|
try {
|
|
135
171
|
while (this.pending && !this.stopped) {
|
|
136
172
|
this.pending = false;
|
|
137
173
|
try {
|
|
138
174
|
await this.sync();
|
|
175
|
+
failure = void 0;
|
|
139
176
|
} catch (error) {
|
|
177
|
+
failure = error;
|
|
140
178
|
this.logger.warn(`WebMCP sync failed: ${toAgentError(error).message}`);
|
|
141
179
|
}
|
|
142
180
|
}
|
|
181
|
+
if (!this.stopped) syncListenerOf(this.modelContext)?.synced(failure);
|
|
143
182
|
} finally {
|
|
144
183
|
this.run = void 0;
|
|
145
184
|
}
|
|
@@ -230,10 +269,7 @@ var WebMcpBridge = class {
|
|
|
230
269
|
throw toAgentError(error);
|
|
231
270
|
}
|
|
232
271
|
if (result.isError) throw new Error(textOf(result.content) || `Tool "${mcpName}" failed`);
|
|
233
|
-
return
|
|
234
|
-
content: result.content ?? [],
|
|
235
|
-
...result.structuredContent !== void 0 && { structuredContent: result.structuredContent }
|
|
236
|
-
};
|
|
272
|
+
return toAgentResult(result, this.options.result);
|
|
237
273
|
}
|
|
238
274
|
/**
|
|
239
275
|
* Run a flow as its own request. A browser build without AsyncContext runs requests one at a
|
|
@@ -284,7 +320,15 @@ var webMcpPluginOptionsSchema = z.object({
|
|
|
284
320
|
/** Auth context the tools are listed and called with. Defaults to an anonymous `webmcp` caller. */
|
|
285
321
|
authContext: z.custom((value) => typeof value === "function" || typeof value === "object" && value !== null).optional(),
|
|
286
322
|
/** The `ModelContext` to register with. Defaults to `document.modelContext`; pass a polyfill or test double. */
|
|
287
|
-
modelContext: z.custom(isModelContext, { message: "modelContext must have a registerTool function" }).optional()
|
|
323
|
+
modelContext: z.custom(isModelContext, { message: "modelContext must have a registerTool function" }).optional(),
|
|
324
|
+
/**
|
|
325
|
+
* What a call resolves to. The agent reads the whole value as text, so by default it gets each result once:
|
|
326
|
+
* - `'structured'`: the tool's `structuredContent` alone when its content only repeats it (one text block with
|
|
327
|
+
* the same JSON, as the server writes it); otherwise `{ content }`, plus `structuredContent` when there is one.
|
|
328
|
+
* - `'content'`: `{ content }`.
|
|
329
|
+
* - `'both'`: `{ content, structuredContent }`, as an MCP client gets them.
|
|
330
|
+
*/
|
|
331
|
+
result: z.enum(["structured", "content", "both"]).default("structured")
|
|
288
332
|
});
|
|
289
333
|
|
|
290
334
|
// plugins/plugin-webmcp/src/webmcp.plugin.ts
|
|
@@ -321,11 +365,168 @@ WebMcpPlugin = __decorateClass([
|
|
|
321
365
|
description: "Exposes the server's tools to in-browser agents through WebMCP (document.modelContext)"
|
|
322
366
|
})
|
|
323
367
|
], WebMcpPlugin);
|
|
368
|
+
|
|
369
|
+
// plugins/plugin-webmcp/src/webmcp.lazy.ts
|
|
370
|
+
var NO_PLUGIN_MESSAGE = "The server has no WebMcpPlugin on the modelContext it was given: pass it to WebMcpPlugin.init({ modelContext })";
|
|
371
|
+
function createPluginSync() {
|
|
372
|
+
let started = false;
|
|
373
|
+
let resolveSynced;
|
|
374
|
+
let rejectSynced;
|
|
375
|
+
const synced = new Promise((resolve, reject) => {
|
|
376
|
+
resolveSynced = resolve;
|
|
377
|
+
rejectSynced = reject;
|
|
378
|
+
});
|
|
379
|
+
synced.catch(() => void 0);
|
|
380
|
+
return {
|
|
381
|
+
listener: {
|
|
382
|
+
started: () => {
|
|
383
|
+
started = true;
|
|
384
|
+
},
|
|
385
|
+
synced: (error) => error === void 0 ? resolveSynced?.() : rejectSynced?.(error)
|
|
386
|
+
},
|
|
387
|
+
async whenSynced() {
|
|
388
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
389
|
+
if (!started) throw new Error(NO_PLUGIN_MESSAGE);
|
|
390
|
+
await synced;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function fingerprintOf(descriptor, exposedTo) {
|
|
395
|
+
return JSON.stringify([descriptor, exposedTo ?? []]);
|
|
396
|
+
}
|
|
397
|
+
async function listWebMcpTools(createServer) {
|
|
398
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
399
|
+
const pluginSync = createPluginSync();
|
|
400
|
+
const modelContext = withSyncListener(
|
|
401
|
+
{
|
|
402
|
+
async registerTool({ execute: _execute, ...descriptor }, options) {
|
|
403
|
+
descriptors.set(descriptor.name, descriptor);
|
|
404
|
+
options?.signal?.addEventListener("abort", () => descriptors.delete(descriptor.name), { once: true });
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
pluginSync.listener
|
|
408
|
+
);
|
|
409
|
+
const server = await createServer(modelContext);
|
|
410
|
+
try {
|
|
411
|
+
await pluginSync.whenSynced();
|
|
412
|
+
return [...descriptors.values()];
|
|
413
|
+
} finally {
|
|
414
|
+
await server.dispose();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
var LazyWebMcpTools = class {
|
|
418
|
+
constructor(modelContext, loadServer) {
|
|
419
|
+
this.modelContext = modelContext;
|
|
420
|
+
this.loadServer = loadServer;
|
|
421
|
+
}
|
|
422
|
+
modelContext;
|
|
423
|
+
loadServer;
|
|
424
|
+
placeholders = /* @__PURE__ */ new Map();
|
|
425
|
+
/** The plugin's `execute` of every tool it registered, by name. */
|
|
426
|
+
executors = /* @__PURE__ */ new Map();
|
|
427
|
+
loading;
|
|
428
|
+
async register(tools, exposedTo) {
|
|
429
|
+
await Promise.all(tools.map((tool) => this.registerPlaceholder(tool, exposedTo)));
|
|
430
|
+
}
|
|
431
|
+
async registerPlaceholder(tool, exposedTo) {
|
|
432
|
+
const placeholder = {
|
|
433
|
+
fingerprint: fingerprintOf(tool, exposedTo),
|
|
434
|
+
controller: new AbortController(),
|
|
435
|
+
adopted: false
|
|
436
|
+
};
|
|
437
|
+
this.placeholders.set(tool.name, placeholder);
|
|
438
|
+
try {
|
|
439
|
+
await this.modelContext.registerTool(
|
|
440
|
+
{ ...tool, execute: (input, options) => this.call(tool.name, input, options) },
|
|
441
|
+
{ signal: placeholder.controller.signal, ...exposedTo && { exposedTo } }
|
|
442
|
+
);
|
|
443
|
+
} catch {
|
|
444
|
+
this.release(tool.name, placeholder);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async call(name, input, options) {
|
|
448
|
+
await this.load();
|
|
449
|
+
const execute = this.executors.get(name);
|
|
450
|
+
if (!execute) throw new Error(`Tool "${name}" not found`);
|
|
451
|
+
return execute(input, options);
|
|
452
|
+
}
|
|
453
|
+
/** Load the server once; a failed load is tried again on the next call. */
|
|
454
|
+
load() {
|
|
455
|
+
this.loading ??= this.loadThroughPlugin().catch((error) => {
|
|
456
|
+
this.loading = void 0;
|
|
457
|
+
throw error;
|
|
458
|
+
});
|
|
459
|
+
return this.loading;
|
|
460
|
+
}
|
|
461
|
+
async loadThroughPlugin() {
|
|
462
|
+
const pluginSync = createPluginSync();
|
|
463
|
+
const attempt = { failed: false };
|
|
464
|
+
const handoff = withSyncListener(
|
|
465
|
+
{ registerTool: (tool, options) => this.adopt(tool, options, attempt) },
|
|
466
|
+
pluginSync.listener
|
|
467
|
+
);
|
|
468
|
+
const server = await this.loadServer(handoff);
|
|
469
|
+
try {
|
|
470
|
+
await pluginSync.whenSynced();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await this.abandon(attempt, server);
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
for (const [name, placeholder] of this.placeholders) {
|
|
476
|
+
if (!placeholder.adopted) this.release(name, placeholder);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** Forget what a failed load's plugin registered, keep the listed tools for the next try, and dispose its server. */
|
|
480
|
+
async abandon(attempt, server) {
|
|
481
|
+
attempt.failed = true;
|
|
482
|
+
this.executors.clear();
|
|
483
|
+
for (const placeholder of this.placeholders.values()) placeholder.adopted = false;
|
|
484
|
+
const disposable = server;
|
|
485
|
+
if (typeof disposable?.dispose === "function") await Promise.resolve(disposable.dispose()).catch(() => void 0);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* A registration from the plugin: an unchanged listed tool keeps its registration and runs the plugin's
|
|
489
|
+
* `execute`; anything else replaces the listed tool of that name, or is new, and goes to the page's context.
|
|
490
|
+
*/
|
|
491
|
+
async adopt(tool, options, attempt) {
|
|
492
|
+
if (attempt.failed || options?.signal?.aborted) return;
|
|
493
|
+
const { execute, ...descriptor } = tool;
|
|
494
|
+
const placeholder = this.placeholders.get(tool.name);
|
|
495
|
+
const kept = placeholder && !placeholder.adopted && placeholder.fingerprint === fingerprintOf(descriptor, options?.exposedTo) ? placeholder : void 0;
|
|
496
|
+
this.executors.set(tool.name, execute);
|
|
497
|
+
options?.signal?.addEventListener(
|
|
498
|
+
"abort",
|
|
499
|
+
() => {
|
|
500
|
+
if (attempt.failed) return;
|
|
501
|
+
if (this.executors.get(tool.name) === execute) this.executors.delete(tool.name);
|
|
502
|
+
if (kept) this.release(tool.name, kept);
|
|
503
|
+
},
|
|
504
|
+
{ once: true }
|
|
505
|
+
);
|
|
506
|
+
if (kept) {
|
|
507
|
+
kept.adopted = true;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (placeholder) this.release(tool.name, placeholder);
|
|
511
|
+
await this.modelContext.registerTool(tool, options);
|
|
512
|
+
}
|
|
513
|
+
release(name, placeholder) {
|
|
514
|
+
placeholder.controller.abort();
|
|
515
|
+
if (this.placeholders.get(name) === placeholder) this.placeholders.delete(name);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
async function registerWebMcpTools(modelContext, tools, loadServer, options = {}) {
|
|
519
|
+
if (!modelContext) return;
|
|
520
|
+
await new LazyWebMcpTools(modelContext, loadServer).register(tools, options.exposedTo);
|
|
521
|
+
}
|
|
324
522
|
export {
|
|
325
523
|
WebMcpBridge,
|
|
326
524
|
WebMcpPlugin,
|
|
327
525
|
WebMcpPlugin as default,
|
|
328
526
|
isWebMcpSupported,
|
|
527
|
+
listWebMcpTools,
|
|
528
|
+
registerWebMcpTools,
|
|
529
|
+
resolveDocumentModelContext,
|
|
329
530
|
toWebMcpToolName,
|
|
330
531
|
webMcpPluginOptionsSchema
|
|
331
532
|
};
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-webmcp",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "WebMCP plugin for FrontMCP - expose an in-browser FrontMCP server's tools to browser agents through document.modelContext",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/agentfront/frontmcp/issues"
|
|
27
27
|
},
|
|
28
|
-
"homepage": "https://
|
|
28
|
+
"homepage": "https://frontmcp.dev/reference/plugins/webmcp",
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public",
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -46,14 +46,24 @@
|
|
|
46
46
|
"types": "../index.d.ts",
|
|
47
47
|
"default": "./index.mjs"
|
|
48
48
|
}
|
|
49
|
+
},
|
|
50
|
+
"./register": {
|
|
51
|
+
"require": {
|
|
52
|
+
"types": "../register.d.ts",
|
|
53
|
+
"default": "../register.js"
|
|
54
|
+
},
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "../register.d.ts",
|
|
57
|
+
"default": "./register.mjs"
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.9.
|
|
53
|
-
"@frontmcp/utils": "1.9.
|
|
62
|
+
"@frontmcp/sdk": "1.9.5",
|
|
63
|
+
"@frontmcp/utils": "1.9.5"
|
|
54
64
|
},
|
|
55
65
|
"peerDependencies": {
|
|
56
|
-
"@frontmcp/lazy-zod": "1.9.
|
|
66
|
+
"@frontmcp/lazy-zod": "1.9.5"
|
|
57
67
|
},
|
|
58
68
|
"devDependencies": {
|
|
59
69
|
"reflect-metadata": "^0.2.2"
|
package/esm/register.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// plugins/plugin-webmcp/src/webmcp.model-context.ts
|
|
2
|
+
function resolveDocumentModelContext() {
|
|
3
|
+
const modelContext = globalThis.document?.modelContext;
|
|
4
|
+
return typeof modelContext === "object" && modelContext !== null && typeof modelContext.registerTool === "function" ? modelContext : void 0;
|
|
5
|
+
}
|
|
6
|
+
function isWebMcpSupported() {
|
|
7
|
+
return resolveDocumentModelContext() !== void 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// plugins/plugin-webmcp/src/webmcp.handoff.ts
|
|
11
|
+
var SYNC_LISTENER = /* @__PURE__ */ Symbol.for("@frontmcp/plugin-webmcp/sync-listener");
|
|
12
|
+
function withSyncListener(modelContext, listener) {
|
|
13
|
+
return Object.assign(modelContext, { [SYNC_LISTENER]: listener });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// plugins/plugin-webmcp/src/webmcp.lazy.ts
|
|
17
|
+
var NO_PLUGIN_MESSAGE = "The server has no WebMcpPlugin on the modelContext it was given: pass it to WebMcpPlugin.init({ modelContext })";
|
|
18
|
+
function createPluginSync() {
|
|
19
|
+
let started = false;
|
|
20
|
+
let resolveSynced;
|
|
21
|
+
let rejectSynced;
|
|
22
|
+
const synced = new Promise((resolve, reject) => {
|
|
23
|
+
resolveSynced = resolve;
|
|
24
|
+
rejectSynced = reject;
|
|
25
|
+
});
|
|
26
|
+
synced.catch(() => void 0);
|
|
27
|
+
return {
|
|
28
|
+
listener: {
|
|
29
|
+
started: () => {
|
|
30
|
+
started = true;
|
|
31
|
+
},
|
|
32
|
+
synced: (error) => error === void 0 ? resolveSynced?.() : rejectSynced?.(error)
|
|
33
|
+
},
|
|
34
|
+
async whenSynced() {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
36
|
+
if (!started) throw new Error(NO_PLUGIN_MESSAGE);
|
|
37
|
+
await synced;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function fingerprintOf(descriptor, exposedTo) {
|
|
42
|
+
return JSON.stringify([descriptor, exposedTo ?? []]);
|
|
43
|
+
}
|
|
44
|
+
var LazyWebMcpTools = class {
|
|
45
|
+
constructor(modelContext, loadServer) {
|
|
46
|
+
this.modelContext = modelContext;
|
|
47
|
+
this.loadServer = loadServer;
|
|
48
|
+
}
|
|
49
|
+
modelContext;
|
|
50
|
+
loadServer;
|
|
51
|
+
placeholders = /* @__PURE__ */ new Map();
|
|
52
|
+
/** The plugin's `execute` of every tool it registered, by name. */
|
|
53
|
+
executors = /* @__PURE__ */ new Map();
|
|
54
|
+
loading;
|
|
55
|
+
async register(tools, exposedTo) {
|
|
56
|
+
await Promise.all(tools.map((tool) => this.registerPlaceholder(tool, exposedTo)));
|
|
57
|
+
}
|
|
58
|
+
async registerPlaceholder(tool, exposedTo) {
|
|
59
|
+
const placeholder = {
|
|
60
|
+
fingerprint: fingerprintOf(tool, exposedTo),
|
|
61
|
+
controller: new AbortController(),
|
|
62
|
+
adopted: false
|
|
63
|
+
};
|
|
64
|
+
this.placeholders.set(tool.name, placeholder);
|
|
65
|
+
try {
|
|
66
|
+
await this.modelContext.registerTool(
|
|
67
|
+
{ ...tool, execute: (input, options) => this.call(tool.name, input, options) },
|
|
68
|
+
{ signal: placeholder.controller.signal, ...exposedTo && { exposedTo } }
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
this.release(tool.name, placeholder);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async call(name, input, options) {
|
|
75
|
+
await this.load();
|
|
76
|
+
const execute = this.executors.get(name);
|
|
77
|
+
if (!execute) throw new Error(`Tool "${name}" not found`);
|
|
78
|
+
return execute(input, options);
|
|
79
|
+
}
|
|
80
|
+
/** Load the server once; a failed load is tried again on the next call. */
|
|
81
|
+
load() {
|
|
82
|
+
this.loading ??= this.loadThroughPlugin().catch((error) => {
|
|
83
|
+
this.loading = void 0;
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
return this.loading;
|
|
87
|
+
}
|
|
88
|
+
async loadThroughPlugin() {
|
|
89
|
+
const pluginSync = createPluginSync();
|
|
90
|
+
const attempt = { failed: false };
|
|
91
|
+
const handoff = withSyncListener(
|
|
92
|
+
{ registerTool: (tool, options) => this.adopt(tool, options, attempt) },
|
|
93
|
+
pluginSync.listener
|
|
94
|
+
);
|
|
95
|
+
const server = await this.loadServer(handoff);
|
|
96
|
+
try {
|
|
97
|
+
await pluginSync.whenSynced();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await this.abandon(attempt, server);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
for (const [name, placeholder] of this.placeholders) {
|
|
103
|
+
if (!placeholder.adopted) this.release(name, placeholder);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Forget what a failed load's plugin registered, keep the listed tools for the next try, and dispose its server. */
|
|
107
|
+
async abandon(attempt, server) {
|
|
108
|
+
attempt.failed = true;
|
|
109
|
+
this.executors.clear();
|
|
110
|
+
for (const placeholder of this.placeholders.values()) placeholder.adopted = false;
|
|
111
|
+
const disposable = server;
|
|
112
|
+
if (typeof disposable?.dispose === "function") await Promise.resolve(disposable.dispose()).catch(() => void 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A registration from the plugin: an unchanged listed tool keeps its registration and runs the plugin's
|
|
116
|
+
* `execute`; anything else replaces the listed tool of that name, or is new, and goes to the page's context.
|
|
117
|
+
*/
|
|
118
|
+
async adopt(tool, options, attempt) {
|
|
119
|
+
if (attempt.failed || options?.signal?.aborted) return;
|
|
120
|
+
const { execute, ...descriptor } = tool;
|
|
121
|
+
const placeholder = this.placeholders.get(tool.name);
|
|
122
|
+
const kept = placeholder && !placeholder.adopted && placeholder.fingerprint === fingerprintOf(descriptor, options?.exposedTo) ? placeholder : void 0;
|
|
123
|
+
this.executors.set(tool.name, execute);
|
|
124
|
+
options?.signal?.addEventListener(
|
|
125
|
+
"abort",
|
|
126
|
+
() => {
|
|
127
|
+
if (attempt.failed) return;
|
|
128
|
+
if (this.executors.get(tool.name) === execute) this.executors.delete(tool.name);
|
|
129
|
+
if (kept) this.release(tool.name, kept);
|
|
130
|
+
},
|
|
131
|
+
{ once: true }
|
|
132
|
+
);
|
|
133
|
+
if (kept) {
|
|
134
|
+
kept.adopted = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (placeholder) this.release(tool.name, placeholder);
|
|
138
|
+
await this.modelContext.registerTool(tool, options);
|
|
139
|
+
}
|
|
140
|
+
release(name, placeholder) {
|
|
141
|
+
placeholder.controller.abort();
|
|
142
|
+
if (this.placeholders.get(name) === placeholder) this.placeholders.delete(name);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
async function registerWebMcpTools(modelContext, tools, loadServer, options = {}) {
|
|
146
|
+
if (!modelContext) return;
|
|
147
|
+
await new LazyWebMcpTools(modelContext, loadServer).register(tools, options.exposedTo);
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
isWebMcpSupported,
|
|
151
|
+
registerWebMcpTools,
|
|
152
|
+
resolveDocumentModelContext
|
|
153
|
+
};
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default, default as WebMcpPlugin } from './webmcp.plugin';
|
|
2
|
-
export { WebMcpBridge,
|
|
3
|
-
export {
|
|
2
|
+
export { WebMcpBridge, toWebMcpToolName, type WebMcpContentResult, type WebMcpToolResult } from './webmcp.bridge';
|
|
3
|
+
export { isWebMcpSupported, resolveDocumentModelContext } from './webmcp.model-context';
|
|
4
|
+
export { listWebMcpTools, registerWebMcpTools, type RegisterWebMcpToolsOptions, type WebMcpServerFactory, type WebMcpToolDescriptor, } from './webmcp.lazy';
|
|
5
|
+
export { webMcpPluginOptionsSchema, type WebMcpAuthContext, type WebMcpListedTool, type WebMcpPluginOptions, type WebMcpPluginOptionsInput, type WebMcpResultMode, } from './webmcp.options';
|
|
4
6
|
export type { ModelContext, ModelContextRegisterToolOptions, ModelContextRegisteredTool, ModelContextTool, ModelContextToolAnnotations, ModelContextToolExecuteOptions, } from './webmcp.types';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAClH,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,YAAY,EACZ,+BAA+B,EAC/B,0BAA0B,EAC1B,gBAAgB,EAChB,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC"}
|
package/index.js
CHANGED
|
@@ -32,6 +32,9 @@ __export(index_exports, {
|
|
|
32
32
|
WebMcpPlugin: () => WebMcpPlugin,
|
|
33
33
|
default: () => WebMcpPlugin,
|
|
34
34
|
isWebMcpSupported: () => isWebMcpSupported,
|
|
35
|
+
listWebMcpTools: () => listWebMcpTools,
|
|
36
|
+
registerWebMcpTools: () => registerWebMcpTools,
|
|
37
|
+
resolveDocumentModelContext: () => resolveDocumentModelContext,
|
|
35
38
|
toWebMcpToolName: () => toWebMcpToolName,
|
|
36
39
|
webMcpPluginOptionsSchema: () => webMcpPluginOptionsSchema
|
|
37
40
|
});
|
|
@@ -43,9 +46,17 @@ var import_sdk2 = require("@frontmcp/sdk");
|
|
|
43
46
|
// plugins/plugin-webmcp/src/webmcp.bridge.ts
|
|
44
47
|
var import_sdk = require("@frontmcp/sdk");
|
|
45
48
|
var import_utils = require("@frontmcp/utils");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
var
|
|
49
|
+
|
|
50
|
+
// plugins/plugin-webmcp/src/webmcp.handoff.ts
|
|
51
|
+
var SYNC_LISTENER = /* @__PURE__ */ Symbol.for("@frontmcp/plugin-webmcp/sync-listener");
|
|
52
|
+
function withSyncListener(modelContext, listener) {
|
|
53
|
+
return Object.assign(modelContext, { [SYNC_LISTENER]: listener });
|
|
54
|
+
}
|
|
55
|
+
function syncListenerOf(modelContext) {
|
|
56
|
+
return modelContext?.[SYNC_LISTENER];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// plugins/plugin-webmcp/src/webmcp.model-context.ts
|
|
49
60
|
function resolveDocumentModelContext() {
|
|
50
61
|
const modelContext = globalThis.document?.modelContext;
|
|
51
62
|
return typeof modelContext === "object" && modelContext !== null && typeof modelContext.registerTool === "function" ? modelContext : void 0;
|
|
@@ -53,6 +64,11 @@ function resolveDocumentModelContext() {
|
|
|
53
64
|
function isWebMcpSupported() {
|
|
54
65
|
return resolveDocumentModelContext() !== void 0;
|
|
55
66
|
}
|
|
67
|
+
|
|
68
|
+
// plugins/plugin-webmcp/src/webmcp.bridge.ts
|
|
69
|
+
var MAX_WEBMCP_NAME_LENGTH = 128;
|
|
70
|
+
var INVALID_NAME_CHARACTERS = /[^A-Za-z0-9_.-]/g;
|
|
71
|
+
var ANONYMOUS_SUBJECT = "anon:webmcp";
|
|
56
72
|
function toWebMcpToolName(name) {
|
|
57
73
|
const cleaned = name.replace(INVALID_NAME_CHARACTERS, "_").slice(0, MAX_WEBMCP_NAME_LENGTH);
|
|
58
74
|
return cleaned.length > 0 ? cleaned : "_";
|
|
@@ -72,6 +88,27 @@ function toWebMcpAnnotations(annotations) {
|
|
|
72
88
|
if (annotations?.openWorldHint === true) hints.untrustedContentHint = true;
|
|
73
89
|
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
74
90
|
}
|
|
91
|
+
function isSameJson(text, value) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.stringify(JSON.parse(text)) === JSON.stringify(value);
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isTextCopyOf(content, structuredContent) {
|
|
99
|
+
const [block, ...others] = content;
|
|
100
|
+
if (block?.type !== "text" || others.length > 0) return false;
|
|
101
|
+
if (isSameJson(block.text, structuredContent)) return true;
|
|
102
|
+
const keys = Object.keys(structuredContent);
|
|
103
|
+
return keys.length === 1 && keys[0] === "content" && block.text === String(structuredContent["content"] ?? "");
|
|
104
|
+
}
|
|
105
|
+
function toAgentResult(result, mode) {
|
|
106
|
+
const content = result.content ?? [];
|
|
107
|
+
const structuredContent = result.structuredContent;
|
|
108
|
+
if (mode === "content" || structuredContent === void 0) return { content };
|
|
109
|
+
if (mode === "structured" && isTextCopyOf(content, structuredContent)) return structuredContent;
|
|
110
|
+
return { content, structuredContent };
|
|
111
|
+
}
|
|
75
112
|
function textOf(content) {
|
|
76
113
|
return (content ?? []).map((block) => block.type === "text" ? block.text : "").filter((text) => text.length > 0).join("\n");
|
|
77
114
|
}
|
|
@@ -128,6 +165,7 @@ var WebMcpBridge = class {
|
|
|
128
165
|
return;
|
|
129
166
|
}
|
|
130
167
|
this.modelContext = modelContext;
|
|
168
|
+
syncListenerOf(modelContext)?.started();
|
|
131
169
|
this.unsubscribe = this.scope.tools.subscribe({ immediate: true }, () => this.refresh());
|
|
132
170
|
}
|
|
133
171
|
/**
|
|
@@ -156,15 +194,19 @@ var WebMcpBridge = class {
|
|
|
156
194
|
/** Sync until no change is pending. Starts a microtask later, so a burst of changes syncs once. */
|
|
157
195
|
async drain() {
|
|
158
196
|
await Promise.resolve();
|
|
197
|
+
let failure;
|
|
159
198
|
try {
|
|
160
199
|
while (this.pending && !this.stopped) {
|
|
161
200
|
this.pending = false;
|
|
162
201
|
try {
|
|
163
202
|
await this.sync();
|
|
203
|
+
failure = void 0;
|
|
164
204
|
} catch (error) {
|
|
205
|
+
failure = error;
|
|
165
206
|
this.logger.warn(`WebMCP sync failed: ${toAgentError(error).message}`);
|
|
166
207
|
}
|
|
167
208
|
}
|
|
209
|
+
if (!this.stopped) syncListenerOf(this.modelContext)?.synced(failure);
|
|
168
210
|
} finally {
|
|
169
211
|
this.run = void 0;
|
|
170
212
|
}
|
|
@@ -255,10 +297,7 @@ var WebMcpBridge = class {
|
|
|
255
297
|
throw toAgentError(error);
|
|
256
298
|
}
|
|
257
299
|
if (result.isError) throw new Error(textOf(result.content) || `Tool "${mcpName}" failed`);
|
|
258
|
-
return
|
|
259
|
-
content: result.content ?? [],
|
|
260
|
-
...result.structuredContent !== void 0 && { structuredContent: result.structuredContent }
|
|
261
|
-
};
|
|
300
|
+
return toAgentResult(result, this.options.result);
|
|
262
301
|
}
|
|
263
302
|
/**
|
|
264
303
|
* Run a flow as its own request. A browser build without AsyncContext runs requests one at a
|
|
@@ -309,7 +348,15 @@ var webMcpPluginOptionsSchema = import_lazy_zod.z.object({
|
|
|
309
348
|
/** Auth context the tools are listed and called with. Defaults to an anonymous `webmcp` caller. */
|
|
310
349
|
authContext: import_lazy_zod.z.custom((value) => typeof value === "function" || typeof value === "object" && value !== null).optional(),
|
|
311
350
|
/** The `ModelContext` to register with. Defaults to `document.modelContext`; pass a polyfill or test double. */
|
|
312
|
-
modelContext: import_lazy_zod.z.custom(isModelContext, { message: "modelContext must have a registerTool function" }).optional()
|
|
351
|
+
modelContext: import_lazy_zod.z.custom(isModelContext, { message: "modelContext must have a registerTool function" }).optional(),
|
|
352
|
+
/**
|
|
353
|
+
* What a call resolves to. The agent reads the whole value as text, so by default it gets each result once:
|
|
354
|
+
* - `'structured'`: the tool's `structuredContent` alone when its content only repeats it (one text block with
|
|
355
|
+
* the same JSON, as the server writes it); otherwise `{ content }`, plus `structuredContent` when there is one.
|
|
356
|
+
* - `'content'`: `{ content }`.
|
|
357
|
+
* - `'both'`: `{ content, structuredContent }`, as an MCP client gets them.
|
|
358
|
+
*/
|
|
359
|
+
result: import_lazy_zod.z.enum(["structured", "content", "both"]).default("structured")
|
|
313
360
|
});
|
|
314
361
|
|
|
315
362
|
// plugins/plugin-webmcp/src/webmcp.plugin.ts
|
|
@@ -346,3 +393,157 @@ WebMcpPlugin = __decorateClass([
|
|
|
346
393
|
description: "Exposes the server's tools to in-browser agents through WebMCP (document.modelContext)"
|
|
347
394
|
})
|
|
348
395
|
], WebMcpPlugin);
|
|
396
|
+
|
|
397
|
+
// plugins/plugin-webmcp/src/webmcp.lazy.ts
|
|
398
|
+
var NO_PLUGIN_MESSAGE = "The server has no WebMcpPlugin on the modelContext it was given: pass it to WebMcpPlugin.init({ modelContext })";
|
|
399
|
+
function createPluginSync() {
|
|
400
|
+
let started = false;
|
|
401
|
+
let resolveSynced;
|
|
402
|
+
let rejectSynced;
|
|
403
|
+
const synced = new Promise((resolve, reject) => {
|
|
404
|
+
resolveSynced = resolve;
|
|
405
|
+
rejectSynced = reject;
|
|
406
|
+
});
|
|
407
|
+
synced.catch(() => void 0);
|
|
408
|
+
return {
|
|
409
|
+
listener: {
|
|
410
|
+
started: () => {
|
|
411
|
+
started = true;
|
|
412
|
+
},
|
|
413
|
+
synced: (error) => error === void 0 ? resolveSynced?.() : rejectSynced?.(error)
|
|
414
|
+
},
|
|
415
|
+
async whenSynced() {
|
|
416
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
417
|
+
if (!started) throw new Error(NO_PLUGIN_MESSAGE);
|
|
418
|
+
await synced;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function fingerprintOf(descriptor, exposedTo) {
|
|
423
|
+
return JSON.stringify([descriptor, exposedTo ?? []]);
|
|
424
|
+
}
|
|
425
|
+
async function listWebMcpTools(createServer) {
|
|
426
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
427
|
+
const pluginSync = createPluginSync();
|
|
428
|
+
const modelContext = withSyncListener(
|
|
429
|
+
{
|
|
430
|
+
async registerTool({ execute: _execute, ...descriptor }, options) {
|
|
431
|
+
descriptors.set(descriptor.name, descriptor);
|
|
432
|
+
options?.signal?.addEventListener("abort", () => descriptors.delete(descriptor.name), { once: true });
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
pluginSync.listener
|
|
436
|
+
);
|
|
437
|
+
const server = await createServer(modelContext);
|
|
438
|
+
try {
|
|
439
|
+
await pluginSync.whenSynced();
|
|
440
|
+
return [...descriptors.values()];
|
|
441
|
+
} finally {
|
|
442
|
+
await server.dispose();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
var LazyWebMcpTools = class {
|
|
446
|
+
constructor(modelContext, loadServer) {
|
|
447
|
+
this.modelContext = modelContext;
|
|
448
|
+
this.loadServer = loadServer;
|
|
449
|
+
}
|
|
450
|
+
modelContext;
|
|
451
|
+
loadServer;
|
|
452
|
+
placeholders = /* @__PURE__ */ new Map();
|
|
453
|
+
/** The plugin's `execute` of every tool it registered, by name. */
|
|
454
|
+
executors = /* @__PURE__ */ new Map();
|
|
455
|
+
loading;
|
|
456
|
+
async register(tools, exposedTo) {
|
|
457
|
+
await Promise.all(tools.map((tool) => this.registerPlaceholder(tool, exposedTo)));
|
|
458
|
+
}
|
|
459
|
+
async registerPlaceholder(tool, exposedTo) {
|
|
460
|
+
const placeholder = {
|
|
461
|
+
fingerprint: fingerprintOf(tool, exposedTo),
|
|
462
|
+
controller: new AbortController(),
|
|
463
|
+
adopted: false
|
|
464
|
+
};
|
|
465
|
+
this.placeholders.set(tool.name, placeholder);
|
|
466
|
+
try {
|
|
467
|
+
await this.modelContext.registerTool(
|
|
468
|
+
{ ...tool, execute: (input, options) => this.call(tool.name, input, options) },
|
|
469
|
+
{ signal: placeholder.controller.signal, ...exposedTo && { exposedTo } }
|
|
470
|
+
);
|
|
471
|
+
} catch {
|
|
472
|
+
this.release(tool.name, placeholder);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async call(name, input, options) {
|
|
476
|
+
await this.load();
|
|
477
|
+
const execute = this.executors.get(name);
|
|
478
|
+
if (!execute) throw new Error(`Tool "${name}" not found`);
|
|
479
|
+
return execute(input, options);
|
|
480
|
+
}
|
|
481
|
+
/** Load the server once; a failed load is tried again on the next call. */
|
|
482
|
+
load() {
|
|
483
|
+
this.loading ??= this.loadThroughPlugin().catch((error) => {
|
|
484
|
+
this.loading = void 0;
|
|
485
|
+
throw error;
|
|
486
|
+
});
|
|
487
|
+
return this.loading;
|
|
488
|
+
}
|
|
489
|
+
async loadThroughPlugin() {
|
|
490
|
+
const pluginSync = createPluginSync();
|
|
491
|
+
const attempt = { failed: false };
|
|
492
|
+
const handoff = withSyncListener(
|
|
493
|
+
{ registerTool: (tool, options) => this.adopt(tool, options, attempt) },
|
|
494
|
+
pluginSync.listener
|
|
495
|
+
);
|
|
496
|
+
const server = await this.loadServer(handoff);
|
|
497
|
+
try {
|
|
498
|
+
await pluginSync.whenSynced();
|
|
499
|
+
} catch (error) {
|
|
500
|
+
await this.abandon(attempt, server);
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
for (const [name, placeholder] of this.placeholders) {
|
|
504
|
+
if (!placeholder.adopted) this.release(name, placeholder);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/** Forget what a failed load's plugin registered, keep the listed tools for the next try, and dispose its server. */
|
|
508
|
+
async abandon(attempt, server) {
|
|
509
|
+
attempt.failed = true;
|
|
510
|
+
this.executors.clear();
|
|
511
|
+
for (const placeholder of this.placeholders.values()) placeholder.adopted = false;
|
|
512
|
+
const disposable = server;
|
|
513
|
+
if (typeof disposable?.dispose === "function") await Promise.resolve(disposable.dispose()).catch(() => void 0);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* A registration from the plugin: an unchanged listed tool keeps its registration and runs the plugin's
|
|
517
|
+
* `execute`; anything else replaces the listed tool of that name, or is new, and goes to the page's context.
|
|
518
|
+
*/
|
|
519
|
+
async adopt(tool, options, attempt) {
|
|
520
|
+
if (attempt.failed || options?.signal?.aborted) return;
|
|
521
|
+
const { execute, ...descriptor } = tool;
|
|
522
|
+
const placeholder = this.placeholders.get(tool.name);
|
|
523
|
+
const kept = placeholder && !placeholder.adopted && placeholder.fingerprint === fingerprintOf(descriptor, options?.exposedTo) ? placeholder : void 0;
|
|
524
|
+
this.executors.set(tool.name, execute);
|
|
525
|
+
options?.signal?.addEventListener(
|
|
526
|
+
"abort",
|
|
527
|
+
() => {
|
|
528
|
+
if (attempt.failed) return;
|
|
529
|
+
if (this.executors.get(tool.name) === execute) this.executors.delete(tool.name);
|
|
530
|
+
if (kept) this.release(tool.name, kept);
|
|
531
|
+
},
|
|
532
|
+
{ once: true }
|
|
533
|
+
);
|
|
534
|
+
if (kept) {
|
|
535
|
+
kept.adopted = true;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (placeholder) this.release(tool.name, placeholder);
|
|
539
|
+
await this.modelContext.registerTool(tool, options);
|
|
540
|
+
}
|
|
541
|
+
release(name, placeholder) {
|
|
542
|
+
placeholder.controller.abort();
|
|
543
|
+
if (this.placeholders.get(name) === placeholder) this.placeholders.delete(name);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
async function registerWebMcpTools(modelContext, tools, loadServer, options = {}) {
|
|
547
|
+
if (!modelContext) return;
|
|
548
|
+
await new LazyWebMcpTools(modelContext, loadServer).register(tools, options.exposedTo);
|
|
549
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-webmcp",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "WebMCP plugin for FrontMCP - expose an in-browser FrontMCP server's tools to browser agents through document.modelContext",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/agentfront/frontmcp/issues"
|
|
27
27
|
},
|
|
28
|
-
"homepage": "https://
|
|
28
|
+
"homepage": "https://frontmcp.dev/reference/plugins/webmcp",
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public",
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -46,14 +46,24 @@
|
|
|
46
46
|
"types": "./index.d.ts",
|
|
47
47
|
"default": "./esm/index.mjs"
|
|
48
48
|
}
|
|
49
|
+
},
|
|
50
|
+
"./register": {
|
|
51
|
+
"require": {
|
|
52
|
+
"types": "./register.d.ts",
|
|
53
|
+
"default": "./register.js"
|
|
54
|
+
},
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./register.d.ts",
|
|
57
|
+
"default": "./esm/register.mjs"
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
},
|
|
51
61
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.9.
|
|
53
|
-
"@frontmcp/utils": "1.9.
|
|
62
|
+
"@frontmcp/sdk": "1.9.5",
|
|
63
|
+
"@frontmcp/utils": "1.9.5"
|
|
54
64
|
},
|
|
55
65
|
"peerDependencies": {
|
|
56
|
-
"@frontmcp/lazy-zod": "1.9.
|
|
66
|
+
"@frontmcp/lazy-zod": "1.9.5"
|
|
57
67
|
},
|
|
58
68
|
"devDependencies": {
|
|
59
69
|
"reflect-metadata": "^0.2.2"
|
package/register.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { isWebMcpSupported, resolveDocumentModelContext } from './webmcp.model-context';
|
|
2
|
+
export { registerWebMcpTools, type RegisterWebMcpToolsOptions, type WebMcpServerFactory, type WebMcpToolDescriptor, } from './webmcp.lazy';
|
|
3
|
+
export type { ModelContext, ModelContextRegisterToolOptions, ModelContextTool, ModelContextToolAnnotations, ModelContextToolExecuteOptions, } from './webmcp.types';
|
|
4
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,mBAAmB,EACnB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,YAAY,EACZ,+BAA+B,EAC/B,gBAAgB,EAChB,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,gBAAgB,CAAC"}
|
package/register.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// plugins/plugin-webmcp/src/register.ts
|
|
21
|
+
var register_exports = {};
|
|
22
|
+
__export(register_exports, {
|
|
23
|
+
isWebMcpSupported: () => isWebMcpSupported,
|
|
24
|
+
registerWebMcpTools: () => registerWebMcpTools,
|
|
25
|
+
resolveDocumentModelContext: () => resolveDocumentModelContext
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(register_exports);
|
|
28
|
+
|
|
29
|
+
// plugins/plugin-webmcp/src/webmcp.model-context.ts
|
|
30
|
+
function resolveDocumentModelContext() {
|
|
31
|
+
const modelContext = globalThis.document?.modelContext;
|
|
32
|
+
return typeof modelContext === "object" && modelContext !== null && typeof modelContext.registerTool === "function" ? modelContext : void 0;
|
|
33
|
+
}
|
|
34
|
+
function isWebMcpSupported() {
|
|
35
|
+
return resolveDocumentModelContext() !== void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// plugins/plugin-webmcp/src/webmcp.handoff.ts
|
|
39
|
+
var SYNC_LISTENER = /* @__PURE__ */ Symbol.for("@frontmcp/plugin-webmcp/sync-listener");
|
|
40
|
+
function withSyncListener(modelContext, listener) {
|
|
41
|
+
return Object.assign(modelContext, { [SYNC_LISTENER]: listener });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// plugins/plugin-webmcp/src/webmcp.lazy.ts
|
|
45
|
+
var NO_PLUGIN_MESSAGE = "The server has no WebMcpPlugin on the modelContext it was given: pass it to WebMcpPlugin.init({ modelContext })";
|
|
46
|
+
function createPluginSync() {
|
|
47
|
+
let started = false;
|
|
48
|
+
let resolveSynced;
|
|
49
|
+
let rejectSynced;
|
|
50
|
+
const synced = new Promise((resolve, reject) => {
|
|
51
|
+
resolveSynced = resolve;
|
|
52
|
+
rejectSynced = reject;
|
|
53
|
+
});
|
|
54
|
+
synced.catch(() => void 0);
|
|
55
|
+
return {
|
|
56
|
+
listener: {
|
|
57
|
+
started: () => {
|
|
58
|
+
started = true;
|
|
59
|
+
},
|
|
60
|
+
synced: (error) => error === void 0 ? resolveSynced?.() : rejectSynced?.(error)
|
|
61
|
+
},
|
|
62
|
+
async whenSynced() {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
64
|
+
if (!started) throw new Error(NO_PLUGIN_MESSAGE);
|
|
65
|
+
await synced;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function fingerprintOf(descriptor, exposedTo) {
|
|
70
|
+
return JSON.stringify([descriptor, exposedTo ?? []]);
|
|
71
|
+
}
|
|
72
|
+
var LazyWebMcpTools = class {
|
|
73
|
+
constructor(modelContext, loadServer) {
|
|
74
|
+
this.modelContext = modelContext;
|
|
75
|
+
this.loadServer = loadServer;
|
|
76
|
+
}
|
|
77
|
+
modelContext;
|
|
78
|
+
loadServer;
|
|
79
|
+
placeholders = /* @__PURE__ */ new Map();
|
|
80
|
+
/** The plugin's `execute` of every tool it registered, by name. */
|
|
81
|
+
executors = /* @__PURE__ */ new Map();
|
|
82
|
+
loading;
|
|
83
|
+
async register(tools, exposedTo) {
|
|
84
|
+
await Promise.all(tools.map((tool) => this.registerPlaceholder(tool, exposedTo)));
|
|
85
|
+
}
|
|
86
|
+
async registerPlaceholder(tool, exposedTo) {
|
|
87
|
+
const placeholder = {
|
|
88
|
+
fingerprint: fingerprintOf(tool, exposedTo),
|
|
89
|
+
controller: new AbortController(),
|
|
90
|
+
adopted: false
|
|
91
|
+
};
|
|
92
|
+
this.placeholders.set(tool.name, placeholder);
|
|
93
|
+
try {
|
|
94
|
+
await this.modelContext.registerTool(
|
|
95
|
+
{ ...tool, execute: (input, options) => this.call(tool.name, input, options) },
|
|
96
|
+
{ signal: placeholder.controller.signal, ...exposedTo && { exposedTo } }
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
this.release(tool.name, placeholder);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async call(name, input, options) {
|
|
103
|
+
await this.load();
|
|
104
|
+
const execute = this.executors.get(name);
|
|
105
|
+
if (!execute) throw new Error(`Tool "${name}" not found`);
|
|
106
|
+
return execute(input, options);
|
|
107
|
+
}
|
|
108
|
+
/** Load the server once; a failed load is tried again on the next call. */
|
|
109
|
+
load() {
|
|
110
|
+
this.loading ??= this.loadThroughPlugin().catch((error) => {
|
|
111
|
+
this.loading = void 0;
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
return this.loading;
|
|
115
|
+
}
|
|
116
|
+
async loadThroughPlugin() {
|
|
117
|
+
const pluginSync = createPluginSync();
|
|
118
|
+
const attempt = { failed: false };
|
|
119
|
+
const handoff = withSyncListener(
|
|
120
|
+
{ registerTool: (tool, options) => this.adopt(tool, options, attempt) },
|
|
121
|
+
pluginSync.listener
|
|
122
|
+
);
|
|
123
|
+
const server = await this.loadServer(handoff);
|
|
124
|
+
try {
|
|
125
|
+
await pluginSync.whenSynced();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await this.abandon(attempt, server);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
for (const [name, placeholder] of this.placeholders) {
|
|
131
|
+
if (!placeholder.adopted) this.release(name, placeholder);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Forget what a failed load's plugin registered, keep the listed tools for the next try, and dispose its server. */
|
|
135
|
+
async abandon(attempt, server) {
|
|
136
|
+
attempt.failed = true;
|
|
137
|
+
this.executors.clear();
|
|
138
|
+
for (const placeholder of this.placeholders.values()) placeholder.adopted = false;
|
|
139
|
+
const disposable = server;
|
|
140
|
+
if (typeof disposable?.dispose === "function") await Promise.resolve(disposable.dispose()).catch(() => void 0);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A registration from the plugin: an unchanged listed tool keeps its registration and runs the plugin's
|
|
144
|
+
* `execute`; anything else replaces the listed tool of that name, or is new, and goes to the page's context.
|
|
145
|
+
*/
|
|
146
|
+
async adopt(tool, options, attempt) {
|
|
147
|
+
if (attempt.failed || options?.signal?.aborted) return;
|
|
148
|
+
const { execute, ...descriptor } = tool;
|
|
149
|
+
const placeholder = this.placeholders.get(tool.name);
|
|
150
|
+
const kept = placeholder && !placeholder.adopted && placeholder.fingerprint === fingerprintOf(descriptor, options?.exposedTo) ? placeholder : void 0;
|
|
151
|
+
this.executors.set(tool.name, execute);
|
|
152
|
+
options?.signal?.addEventListener(
|
|
153
|
+
"abort",
|
|
154
|
+
() => {
|
|
155
|
+
if (attempt.failed) return;
|
|
156
|
+
if (this.executors.get(tool.name) === execute) this.executors.delete(tool.name);
|
|
157
|
+
if (kept) this.release(tool.name, kept);
|
|
158
|
+
},
|
|
159
|
+
{ once: true }
|
|
160
|
+
);
|
|
161
|
+
if (kept) {
|
|
162
|
+
kept.adopted = true;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (placeholder) this.release(tool.name, placeholder);
|
|
166
|
+
await this.modelContext.registerTool(tool, options);
|
|
167
|
+
}
|
|
168
|
+
release(name, placeholder) {
|
|
169
|
+
placeholder.controller.abort();
|
|
170
|
+
if (this.placeholders.get(name) === placeholder) this.placeholders.delete(name);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function registerWebMcpTools(modelContext, tools, loadServer, options = {}) {
|
|
174
|
+
if (!modelContext) return;
|
|
175
|
+
await new LazyWebMcpTools(modelContext, loadServer).register(tools, options.exposedTo);
|
|
176
|
+
}
|
package/webmcp.bridge.d.ts
CHANGED
|
@@ -7,16 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type CallToolResult, type ScopeEntry } from '@frontmcp/sdk';
|
|
9
9
|
import type { WebMcpPluginOptions } from './webmcp.options';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export interface WebMcpToolResult {
|
|
10
|
+
/** The MCP result without its `_meta` (and never `isError`). */
|
|
11
|
+
export interface WebMcpContentResult {
|
|
13
12
|
content: CallToolResult['content'];
|
|
14
13
|
structuredContent?: CallToolResult['structuredContent'];
|
|
15
14
|
}
|
|
16
|
-
/**
|
|
17
|
-
export
|
|
18
|
-
/** Whether this page can register WebMCP tools (`document.modelContext.registerTool` exists). */
|
|
19
|
-
export declare function isWebMcpSupported(): boolean;
|
|
15
|
+
/** What a WebMCP call resolves to: the tool's structured content alone, or its content (see the `result` option). */
|
|
16
|
+
export type WebMcpToolResult = WebMcpContentResult | NonNullable<CallToolResult['structuredContent']>;
|
|
20
17
|
/** Make `name` a valid WebMCP tool name: invalid characters become `_`, cut to 128 characters. */
|
|
21
18
|
export declare function toWebMcpToolName(name: string): string;
|
|
22
19
|
export declare class WebMcpBridge {
|
package/webmcp.bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp.bridge.d.ts","sourceRoot":"","sources":["../src/webmcp.bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"webmcp.bridge.d.ts","sourceRoot":"","sources":["../src/webmcp.bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAKvB,OAAO,KAAK,EAAoB,mBAAmB,EAAoB,MAAM,kBAAkB,CAAC;AAYhG,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,iBAAiB,CAAC,EAAE,cAAc,CAAC,mBAAmB,CAAC,CAAC;CACzD;AAED,qHAAqH;AACrH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,GAAG,WAAW,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC;AA4BtG,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGrD;AAmFD,qBAAa,YAAY;IAgBrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhB1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,gEAAgE;IAChE,OAAO,CAAC,OAAO,CAAS;IACxB,0CAA0C;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAgB;gBAGT,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,mBAAmB;IAK/C,4EAA4E;IAC5E,IAAI,mBAAmB,IAAI,MAAM,EAAE,CAElC;IAED;;;OAGG;IACH,KAAK,IAAI,IAAI;IAcb;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAMf,qDAAqD;IAC/C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,4FAA4F;IAC5F,IAAI,IAAI,IAAI;IASZ,mGAAmG;YACrF,KAAK;YAoBL,IAAI;IAgBlB,kFAAkF;YACpE,SAAS;IAevB,OAAO,CAAC,YAAY;YAmBN,QAAQ;IAwBtB,kGAAkG;YACpF,OAAO;IAwBrB;;;OAGG;YACW,SAAS;IASvB,2FAA2F;YAC7E,QAAQ;CAiBvB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ModelContext } from './webmcp.types';
|
|
2
|
+
export interface WebMcpSyncListener {
|
|
3
|
+
/** The plugin found this `ModelContext` and follows the server's tools on it. */
|
|
4
|
+
started(): void;
|
|
5
|
+
/** The plugin registered the server's current tools; `error` when listing them failed. */
|
|
6
|
+
synced(error?: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function withSyncListener(modelContext: ModelContext, listener: WebMcpSyncListener): ModelContext;
|
|
9
|
+
export declare function syncListenerOf(modelContext: ModelContext | undefined): WebMcpSyncListener | undefined;
|
|
10
|
+
//# sourceMappingURL=webmcp.handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp.handoff.d.ts","sourceRoot":"","sources":["../src/webmcp.handoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,OAAO,IAAI,IAAI,CAAC;IAChB,0FAA0F;IAC1F,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B;AAID,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,GAAG,YAAY,CAEvG;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,YAAY,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,CAErG"}
|
package/webmcp.lazy.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offering a page's tools to agents before its server is loaded: `listWebMcpTools()` computes, at build time,
|
|
3
|
+
* what WebMcpPlugin registers, and `registerWebMcpTools()` registers that list in the page and loads the server
|
|
4
|
+
* on an agent's first call. The calls then run through the plugin. Neither imports the SDK.
|
|
5
|
+
*/
|
|
6
|
+
import type { ModelContext, ModelContextTool } from './webmcp.types';
|
|
7
|
+
/** A tool as WebMcpPlugin passes it to `registerTool()`, without its `execute`. */
|
|
8
|
+
export type WebMcpToolDescriptor = Omit<ModelContextTool, 'execute'>;
|
|
9
|
+
/**
|
|
10
|
+
* Builds the server, passing `modelContext` to `WebMcpPlugin.init({ modelContext })` with the plugin's other
|
|
11
|
+
* options as the page uses them.
|
|
12
|
+
*/
|
|
13
|
+
export type WebMcpServerFactory<Server = unknown> = (modelContext: ModelContext) => Promise<Server>;
|
|
14
|
+
/** Options of `registerWebMcpTools()`. */
|
|
15
|
+
export interface RegisterWebMcpToolsOptions {
|
|
16
|
+
/** Other origins the tools are offered to, as the plugin's `exposedTo` option. */
|
|
17
|
+
exposedTo?: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What WebMcpPlugin registers for the server `createServer` builds: names, titles, descriptions, input schemas
|
|
21
|
+
* and hints, as JSON-safe values. Run it at build time and ship the result to `registerWebMcpTools()`. The
|
|
22
|
+
* server is disposed before this resolves.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const tools = await listWebMcpTools((modelContext) => createShopServer({ modelContext }));
|
|
27
|
+
* await writeFile('webmcp-tools.json', JSON.stringify(tools));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function listWebMcpTools(createServer: WebMcpServerFactory<{
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
}>): Promise<WebMcpToolDescriptor[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Register `tools` (from `listWebMcpTools()`) with `modelContext` at once, without loading the server. An
|
|
35
|
+
* agent's first call loads it with `loadServer` (once, however many calls arrive together) and runs through
|
|
36
|
+
* WebMcpPlugin from then on, which also registers and unregisters tools as the server's tools change. Does
|
|
37
|
+
* nothing when `modelContext` is undefined (no WebMCP in this browser).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { registerWebMcpTools, resolveDocumentModelContext } from '@frontmcp/plugin-webmcp/register';
|
|
42
|
+
* import tools from './webmcp-tools.json';
|
|
43
|
+
*
|
|
44
|
+
* void registerWebMcpTools(resolveDocumentModelContext(), tools, (modelContext) =>
|
|
45
|
+
* import('./server').then(({ createShopServer }) => createShopServer({ modelContext })),
|
|
46
|
+
* );
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerWebMcpTools(modelContext: ModelContext | undefined, tools: readonly WebMcpToolDescriptor[], loadServer: WebMcpServerFactory, options?: RegisterWebMcpToolsOptions): Promise<void>;
|
|
50
|
+
//# sourceMappingURL=webmcp.lazy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp.lazy.d.ts","sourceRoot":"","sources":["../src/webmcp.lazy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,YAAY,EAEZ,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AAExB,mFAAmF;AACnF,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,mBAAmB,CAAC,MAAM,GAAG,OAAO,IAAI,CAAC,YAAY,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEpG,0CAA0C;AAC1C,MAAM,WAAW,0BAA0B;IACzC,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAyCD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,CAAC;IAAE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,GAC9D,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAmBjC;AAmID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,SAAS,oBAAoB,EAAE,EACtC,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModelContext } from './webmcp.types';
|
|
2
|
+
/** `document.modelContext` in a browser that implements WebMCP (or has a polyfill installed). */
|
|
3
|
+
export declare function resolveDocumentModelContext(): ModelContext | undefined;
|
|
4
|
+
/** Whether this page can register WebMCP tools (`document.modelContext.registerTool` exists). */
|
|
5
|
+
export declare function isWebMcpSupported(): boolean;
|
|
6
|
+
//# sourceMappingURL=webmcp.model-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp.model-context.d.ts","sourceRoot":"","sources":["../src/webmcp.model-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,iGAAiG;AACjG,wBAAgB,2BAA2B,IAAI,YAAY,GAAG,SAAS,CAOtE;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C"}
|
package/webmcp.options.d.ts
CHANGED
|
@@ -11,7 +11,14 @@ export declare const webMcpPluginOptionsSchema: import("@frontmcp/lazy-zod").Zod
|
|
|
11
11
|
exposedTo: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodString>>;
|
|
12
12
|
authContext: import("@frontmcp/lazy-zod").ZodOptional<import("zod").ZodCustom<WebMcpAuthContext, WebMcpAuthContext>>;
|
|
13
13
|
modelContext: import("@frontmcp/lazy-zod").ZodOptional<import("zod").ZodCustom<ModelContext, ModelContext>>;
|
|
14
|
+
result: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodEnum<{
|
|
15
|
+
structured: "structured";
|
|
16
|
+
content: "content";
|
|
17
|
+
both: "both";
|
|
18
|
+
}>>;
|
|
14
19
|
}, import("zod/v4/core").$strip>;
|
|
20
|
+
/** What an agent's call resolves to (see the `result` option). */
|
|
21
|
+
export type WebMcpResultMode = z.output<typeof webMcpPluginOptionsSchema>['result'];
|
|
15
22
|
/** Options of `WebMcpPlugin.init()`, as the plugin holds them (defaults applied). */
|
|
16
23
|
export type WebMcpPluginOptions = z.output<typeof webMcpPluginOptionsSchema>;
|
|
17
24
|
/** Options of `WebMcpPlugin.init()`, as passed. */
|
package/webmcp.options.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp.options.d.ts","sourceRoot":"","sources":["../src/webmcp.options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,yFAAyF;AACzF,MAAM,MAAM,gBAAgB,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAM3G,eAAO,MAAM,yBAAyB;;qFAOX,gBAAgB,KAAK,OAAO,SAA5B,gBAAgB,KAAK,OAAO
|
|
1
|
+
{"version":3,"file":"webmcp.options.d.ts","sourceRoot":"","sources":["../src/webmcp.options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,yFAAyF;AACzF,MAAM,MAAM,gBAAgB,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAM3G,eAAO,MAAM,yBAAyB;;qFAOX,gBAAgB,KAAK,OAAO,SAA5B,gBAAgB,KAAK,OAAO;;;;;;;;;gCAmBrD,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEpF,qFAAqF;AACrF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE7E,mDAAmD;AACnD,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|