@mcp-b/webmcp-ts-sdk 5.1.0 → 6.0.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 +17 -18
- package/dist/common-CRzM40Z0.d.ts +22 -0
- package/dist/index.d.ts +127 -18
- package/dist/index.js +207 -228
- package/dist/index.js.map +1 -1
- package/dist/schema-FbP7xLdQ.js +187 -0
- package/dist/schema-FbP7xLdQ.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.js +3 -0
- package/package.json +12 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as createInvalidStateError, i as coerceWebMcpToolDescriptor, n as normalizeInputSchema, o as isPlainObject, r as normalizeToolResponse, s as serializeInputSchema, t as isMcpStandardSchema } from "./schema-FbP7xLdQ.js";
|
|
2
|
+
import { installWebMCP } from "@mcp-b/webmcp-polyfill";
|
|
2
3
|
import { McpServer, ResourceTemplate, fromJsonSchema, isInputRequiredResult, mergeCapabilities } from "@modelcontextprotocol/server";
|
|
3
4
|
|
|
4
5
|
//#region src/browser-server.ts
|
|
@@ -7,48 +8,40 @@ const SERVER_MARKER_PROPERTY = "__isBrowserMcpServer";
|
|
|
7
8
|
function isBrowserMcpServer(context) {
|
|
8
9
|
return typeof context === "object" && context !== null && SERVER_MARKER_PROPERTY in context && context[SERVER_MARKER_PROPERTY] === true;
|
|
9
10
|
}
|
|
11
|
+
function isPeerOriginTransport(transport) {
|
|
12
|
+
return "clientOrigin" in transport && (transport.clientOrigin === void 0 || typeof transport.clientOrigin === "string") && (!("onclientorigin" in transport) || transport.onclientorigin === void 0 || typeof transport.onclientorigin === "function");
|
|
13
|
+
}
|
|
10
14
|
function parseNativeToolResult(serialized) {
|
|
11
|
-
if (serialized === null) return {
|
|
12
|
-
content: [{
|
|
13
|
-
type: "text",
|
|
14
|
-
text: "Tool execution interrupted by navigation"
|
|
15
|
-
}],
|
|
16
|
-
isError: true
|
|
17
|
-
};
|
|
18
15
|
let result;
|
|
19
16
|
try {
|
|
20
17
|
result = JSON.parse(serialized);
|
|
21
18
|
} catch {
|
|
22
|
-
|
|
19
|
+
result = serialized;
|
|
23
20
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
if (isPlainObject(result)) return result;
|
|
22
|
+
return { content: [{
|
|
23
|
+
type: "text",
|
|
24
|
+
text: typeof result === "string" ? result : serialized
|
|
25
|
+
}] };
|
|
29
26
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
/** Runs `operation` in a microtask; rejects with the reason as soon as `signal` aborts. */
|
|
28
|
+
function withAbortSignal(operation, signal) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
const onAbort = () => reject(signal.reason);
|
|
32
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
33
|
+
Promise.resolve().then(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
34
|
+
});
|
|
34
35
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
return
|
|
38
|
-
name: tool.name,
|
|
39
|
-
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
40
|
-
description: tool.description,
|
|
41
|
-
...inputSchema !== void 0 ? { inputSchema } : {},
|
|
42
|
-
...annotations ? { annotations } : {},
|
|
43
|
-
execute
|
|
44
|
-
};
|
|
36
|
+
function toMcpInputSchema(normalized) {
|
|
37
|
+
if (isMcpStandardSchema(normalized.inputSchema)) return normalized.inputSchema;
|
|
38
|
+
return fromJsonSchema(normalized.inputSchema);
|
|
45
39
|
}
|
|
46
40
|
/**
|
|
47
41
|
* Thin WebMCP-to-MCP v2 adapter.
|
|
48
42
|
*
|
|
49
43
|
* The official v2 `McpServer` owns MCP registration, validation, and transport
|
|
50
|
-
* behavior. This class
|
|
51
|
-
* Chrome mirroring.
|
|
44
|
+
* behavior. This class extends the upstream WebMCP context with MCP capabilities.
|
|
52
45
|
*/
|
|
53
46
|
var BrowserMcpServer = class extends EventTarget {
|
|
54
47
|
[SERVER_MARKER_PROPERTY] = true;
|
|
@@ -68,13 +61,23 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
68
61
|
nativeToolChangeListener;
|
|
69
62
|
closed = false;
|
|
70
63
|
closePromise;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
64
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
65
|
+
eventHandlerListener = (event) => {
|
|
66
|
+
this.eventHandlers.get(event.type)?.call(this, event);
|
|
67
|
+
};
|
|
68
|
+
nativeLifecycleListener = (event) => {
|
|
69
|
+
if (this.closed) return;
|
|
70
|
+
const mirrored = new Event(event.type);
|
|
71
|
+
if ("toolName" in event) Object.defineProperty(mirrored, "toolName", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
value: event.toolName
|
|
74
|
+
});
|
|
75
|
+
this.dispatchEvent(mirrored);
|
|
74
76
|
};
|
|
75
77
|
constructor(serverInfo, options = {}) {
|
|
76
78
|
super();
|
|
77
|
-
const { native, ...serverOptions } = options;
|
|
79
|
+
const { native: suppliedContext, ...serverOptions } = options;
|
|
80
|
+
if (!suppliedContext && !globalThis.document?.modelContext) installWebMCP();
|
|
78
81
|
this.mcpServer = new McpServer(serverInfo, {
|
|
79
82
|
...serverOptions,
|
|
80
83
|
capabilities: mergeCapabilities(serverOptions.capabilities ?? {}, {
|
|
@@ -83,59 +86,84 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
83
86
|
prompts: { listChanged: true }
|
|
84
87
|
})
|
|
85
88
|
});
|
|
86
|
-
this.native =
|
|
89
|
+
this.native = suppliedContext ?? globalThis.document?.modelContext;
|
|
87
90
|
this.ownerDocument = globalThis.document ?? null;
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
this.nativeToolChangeListener = () => {
|
|
92
|
+
if (this.closed) return;
|
|
93
|
+
this.nativeToolChangeQueue = this.nativeToolChangeQueue.then(async () => {
|
|
94
|
+
try {
|
|
95
|
+
await this.syncNativeTools();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn("[BrowserMcpServer] Native WebMCP tool reconciliation failed:", error);
|
|
98
|
+
}
|
|
99
|
+
await this.notifyProducerToolsChanged();
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
this.native?.addEventListener("toolchange", this.nativeToolChangeListener);
|
|
103
|
+
this.native?.addEventListener("toolactivated", this.nativeLifecycleListener);
|
|
104
|
+
this.native?.addEventListener("toolcancel", this.nativeLifecycleListener);
|
|
105
|
+
}
|
|
106
|
+
getEventHandler(type) {
|
|
107
|
+
return this.eventHandlers.get(type) ?? null;
|
|
108
|
+
}
|
|
109
|
+
setEventHandler(type, handler) {
|
|
110
|
+
if (typeof handler === "function") {
|
|
111
|
+
if (!this.eventHandlers.has(type)) super.addEventListener(type, this.eventHandlerListener);
|
|
112
|
+
this.eventHandlers.set(type, handler);
|
|
113
|
+
} else if (this.eventHandlers.delete(type)) super.removeEventListener(type, this.eventHandlerListener);
|
|
102
114
|
}
|
|
103
115
|
get ontoolchange() {
|
|
104
|
-
return this.
|
|
116
|
+
return this.getEventHandler("toolchange");
|
|
105
117
|
}
|
|
106
118
|
set ontoolchange(handler) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
this.setEventHandler("toolchange", handler);
|
|
120
|
+
}
|
|
121
|
+
get ontoolactivated() {
|
|
122
|
+
return this.getEventHandler("toolactivated");
|
|
123
|
+
}
|
|
124
|
+
set ontoolactivated(handler) {
|
|
125
|
+
this.setEventHandler("toolactivated", handler);
|
|
126
|
+
}
|
|
127
|
+
get ontoolcancel() {
|
|
128
|
+
return this.getEventHandler("toolcancel");
|
|
129
|
+
}
|
|
130
|
+
set ontoolcancel(handler) {
|
|
131
|
+
this.setEventHandler("toolcancel", handler);
|
|
115
132
|
}
|
|
116
133
|
async notifyProducerToolsChanged() {
|
|
117
134
|
if (this.closed) return;
|
|
118
135
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
119
136
|
if (!this.closed) this.dispatchEvent(new Event("toolchange"));
|
|
120
137
|
}
|
|
121
|
-
getNativeStandardToolsApi() {
|
|
122
|
-
const candidate = this.native;
|
|
123
|
-
return candidate && typeof candidate.executeTool === "function" ? candidate : void 0;
|
|
124
|
-
}
|
|
125
138
|
async registerNativeToolMirror(tool, normalized, options, execute, controller) {
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
const native = this.native;
|
|
140
|
+
if (!native) return;
|
|
128
141
|
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
129
|
-
const
|
|
142
|
+
const nativeTool = {
|
|
143
|
+
name: tool.name,
|
|
144
|
+
description: tool.description,
|
|
145
|
+
execute
|
|
146
|
+
};
|
|
147
|
+
if (tool.title !== void 0) nativeTool.title = tool.title;
|
|
148
|
+
if (normalized.registeredInputSchema !== void 0) {
|
|
149
|
+
const inputSchema = JSON.parse(normalized.registeredInputSchema);
|
|
150
|
+
if (!isPlainObject(inputSchema)) throw new TypeError("inputSchema must be an object");
|
|
151
|
+
nativeTool.inputSchema = inputSchema;
|
|
152
|
+
}
|
|
153
|
+
if (tool.annotations) nativeTool.annotations = tool.annotations;
|
|
130
154
|
this.nativeToolAbortControllers.set(tool.name, controller);
|
|
131
155
|
try {
|
|
132
|
-
await
|
|
156
|
+
await native.registerTool(nativeTool, {
|
|
133
157
|
...options,
|
|
134
158
|
signal
|
|
135
159
|
});
|
|
136
160
|
} catch (error) {
|
|
137
161
|
controller.abort();
|
|
138
162
|
if (this.nativeToolAbortControllers.get(tool.name) === controller) this.nativeToolAbortControllers.delete(tool.name);
|
|
163
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
164
|
+
console.warn(`[BrowserMcpServer] Tool "${tool.name}" is available over MCP only: ${error.message}. Grant allow="tools" on the iframe to expose it to WebMCP.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
139
167
|
throw error;
|
|
140
168
|
}
|
|
141
169
|
if (this.nativeToolAbortControllers.get(tool.name) === controller) this.removingNativeTools.delete(tool.name);
|
|
@@ -146,79 +174,74 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
146
174
|
controller.abort();
|
|
147
175
|
this.nativeToolAbortControllers.delete(name);
|
|
148
176
|
}
|
|
149
|
-
validateToolDescriptor(tool) {
|
|
150
|
-
validateWebMcpToolDescriptor(tool);
|
|
151
|
-
if (this.tools.has(tool.name) || this.pendingTools.has(tool.name)) throw createInvalidStateError(`Tool already registered: ${tool.name}`);
|
|
152
|
-
return normalizeInputSchema(tool.inputSchema);
|
|
153
|
-
}
|
|
154
177
|
registerToolInMcp(tool, normalized, executeTool, exposedTo) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const mcpAnnotations = toMcpAnnotations(tool.annotations);
|
|
158
|
-
const mcpCompatibleInput = normalized.inputSchema.type === void 0 || normalized.inputSchema.type === "object";
|
|
159
|
-
const mcpHandle = mcpCompatibleInput ? this.mcpServer.registerTool(tool.name, {
|
|
160
|
-
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
178
|
+
const item = {
|
|
179
|
+
name: tool.name,
|
|
161
180
|
description: tool.description,
|
|
162
|
-
inputSchema:
|
|
163
|
-
...mcpOutputSchema ? { outputSchema: mcpOutputSchema } : {},
|
|
164
|
-
...mcpAnnotations ? { annotations: mcpAnnotations } : {}
|
|
165
|
-
}, async (args, context) => {
|
|
166
|
-
const result = await executeTool(args, context.mcpReq.signal);
|
|
167
|
-
if (isInputRequiredResult(result)) throw new Error(`WebMCP tool "${tool.name}" returned input_required. Multi-round tool flows require BrowserMcpServer.mcpServer.registerTool().`);
|
|
168
|
-
return normalizeToolResponse(result);
|
|
169
|
-
}) : void 0;
|
|
170
|
-
if (exposedTo?.length) mcpHandle?.disable();
|
|
171
|
-
if (!mcpCompatibleInput) console.warn(`[BrowserMcpServer] Tool "${tool.name}" remains available through WebMCP but cannot be exposed over MCP because MCP input schemas require an object root.`);
|
|
172
|
-
return {
|
|
173
|
-
item: {
|
|
174
|
-
name: tool.name,
|
|
175
|
-
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
176
|
-
description: tool.description,
|
|
177
|
-
inputSchema: normalized.inputSchema,
|
|
178
|
-
...outputSchema === void 0 ? {} : { outputSchema },
|
|
179
|
-
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
180
|
-
},
|
|
181
|
-
...normalized.registeredInputSchema !== void 0 ? { registeredInputSchema: normalized.registeredInputSchema } : {},
|
|
182
|
-
execute: executeTool,
|
|
183
|
-
mcpHandle,
|
|
184
|
-
...exposedTo?.length ? { exposedTo } : {}
|
|
181
|
+
inputSchema: normalized.inputSchema
|
|
185
182
|
};
|
|
183
|
+
if (tool.title !== void 0) item.title = tool.title;
|
|
184
|
+
if (tool.outputSchema !== void 0) item.outputSchema = structuredClone(tool.outputSchema);
|
|
185
|
+
if (tool.annotations) item.annotations = tool.annotations;
|
|
186
|
+
const config = { description: tool.description };
|
|
187
|
+
if (item.outputSchema !== void 0) config.outputSchema = fromJsonSchema(item.outputSchema);
|
|
188
|
+
if (tool.title !== void 0) config.title = tool.title;
|
|
189
|
+
if (tool.annotations) {
|
|
190
|
+
const { untrustedContentHint: _untrustedContentHint, ...annotations } = tool.annotations;
|
|
191
|
+
config.annotations = annotations;
|
|
192
|
+
}
|
|
193
|
+
let mcpHandle;
|
|
194
|
+
if (normalized.inputSchema.type === void 0 || normalized.inputSchema.type === "object") {
|
|
195
|
+
config.inputSchema = toMcpInputSchema(normalized);
|
|
196
|
+
mcpHandle = this.mcpServer.registerTool(tool.name, config, async (args, context) => {
|
|
197
|
+
const result = await executeTool(args, { signal: context.mcpReq.signal });
|
|
198
|
+
if (isInputRequiredResult(result)) throw new Error(`WebMCP tool "${tool.name}" returned input_required. Multi-round tool flows require BrowserMcpServer.mcpServer.registerTool().`);
|
|
199
|
+
return normalizeToolResponse(result);
|
|
200
|
+
});
|
|
201
|
+
mcpHandle.disable();
|
|
202
|
+
} else console.warn(`[BrowserMcpServer] Tool "${tool.name}" remains available through WebMCP but cannot be exposed over MCP because MCP input schemas require an object root.`);
|
|
203
|
+
const registered = {
|
|
204
|
+
item,
|
|
205
|
+
mcpHandle
|
|
206
|
+
};
|
|
207
|
+
if (exposedTo?.length) registered.exposedTo = exposedTo;
|
|
208
|
+
return registered;
|
|
186
209
|
}
|
|
187
210
|
registerTool = async (toolValue, optionsValue = {}) => {
|
|
188
211
|
const { signal, exposedTo } = optionsValue ?? {};
|
|
189
|
-
const options = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
};
|
|
193
|
-
validateWebMcpAccess(this.ownerDocument);
|
|
212
|
+
const options = {};
|
|
213
|
+
if (signal) options.signal = signal;
|
|
214
|
+
if (exposedTo) options.exposedTo = [...exposedTo];
|
|
194
215
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
195
216
|
if (!isPlainObject(toolValue)) throw new TypeError("registerTool(tool) requires a tool object");
|
|
196
217
|
const tool = coerceWebMcpToolDescriptor(toolValue);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
validatePotentiallyTrustworthyOrigins(options.exposedTo);
|
|
218
|
+
if (this.tools.has(tool.name) && !this.nativeBackfilledTools.has(tool.name) || this.pendingTools.has(tool.name)) throw createInvalidStateError(`Tool already registered: ${tool.name}`);
|
|
219
|
+
const normalized = normalizeInputSchema(tool.inputSchema);
|
|
200
220
|
options.signal?.throwIfAborted();
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
return withAbortSignal(Promise.resolve().then(() => Reflect.apply(tool.execute, void 0, [args])), signal);
|
|
204
|
-
};
|
|
221
|
+
const callback = tool.execute;
|
|
222
|
+
const execute = (args, options) => withAbortSignal(() => callback(args, options), options.signal);
|
|
205
223
|
const controller = new AbortController();
|
|
224
|
+
const exposedOrigins = options.exposedTo?.map((origin) => URL.parse(origin)?.origin ?? origin);
|
|
206
225
|
let registered;
|
|
207
226
|
const abort = () => {
|
|
208
227
|
if (this.pendingTools.get(tool.name) === controller) this.pendingTools.delete(tool.name);
|
|
209
228
|
if (registered && this.tools.get(tool.name) === registered) this.removeTool(tool.name);
|
|
210
|
-
else
|
|
229
|
+
else {
|
|
230
|
+
registered?.mcpHandle?.remove();
|
|
231
|
+
this.abortNativeToolMirror(tool.name, controller);
|
|
232
|
+
}
|
|
233
|
+
registered = void 0;
|
|
211
234
|
};
|
|
212
235
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
213
236
|
this.pendingTools.set(tool.name, controller);
|
|
214
237
|
try {
|
|
238
|
+
if (!this.nativeBackfilledTools.has(tool.name)) registered = this.registerToolInMcp(tool, normalized, execute, exposedOrigins);
|
|
239
|
+
options.signal?.throwIfAborted();
|
|
215
240
|
await this.registerNativeToolMirror(tool, normalized, options, execute, controller);
|
|
216
241
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
217
242
|
options.signal?.throwIfAborted();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return parsedOrigin === "null" ? origin : parsedOrigin;
|
|
221
|
-
}));
|
|
243
|
+
this.removeTool(tool.name, { skipNative: true });
|
|
244
|
+
registered ??= this.registerToolInMcp(tool, normalized, execute, exposedOrigins);
|
|
222
245
|
this.tools.set(tool.name, registered);
|
|
223
246
|
this.applyToolExposure();
|
|
224
247
|
if (options.signal?.aborted) {
|
|
@@ -226,6 +249,7 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
226
249
|
options.signal.throwIfAborted();
|
|
227
250
|
}
|
|
228
251
|
} catch (error) {
|
|
252
|
+
registered?.mcpHandle?.remove();
|
|
229
253
|
this.abortNativeToolMirror(tool.name, controller);
|
|
230
254
|
options.signal?.removeEventListener("abort", abort);
|
|
231
255
|
options.signal?.throwIfAborted();
|
|
@@ -237,8 +261,7 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
237
261
|
registered.abortSignal = options.signal;
|
|
238
262
|
registered.abortListener = abort;
|
|
239
263
|
}
|
|
240
|
-
if (this.tools.get(tool.name) === registered)
|
|
241
|
-
else await this.notifyProducerToolsChanged();
|
|
264
|
+
if (this.tools.get(tool.name) === registered) await this.nativeToolChangeQueue;
|
|
242
265
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
243
266
|
options.signal?.throwIfAborted();
|
|
244
267
|
};
|
|
@@ -249,46 +272,52 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
249
272
|
this.tools.delete(name);
|
|
250
273
|
this.nativeBackfilledTools.delete(name);
|
|
251
274
|
registered.mcpHandle?.remove();
|
|
252
|
-
if ((options?.notify ?? true) && !this.native) this.notifyProducerToolsChanged();
|
|
253
275
|
}
|
|
254
276
|
if (!options?.skipNative) this.abortNativeToolMirror(name);
|
|
255
277
|
}
|
|
256
278
|
syncNativeTools() {
|
|
279
|
+
const native = this.native;
|
|
257
280
|
const sync = this.nativeSyncQueue.then(async () => {
|
|
258
|
-
if (this.closed) return;
|
|
259
|
-
|
|
260
|
-
if (native) await this.backfillNativeStandardTools(native);
|
|
281
|
+
if (this.closed || !native) return;
|
|
282
|
+
await this.backfillNativeStandardTools(native);
|
|
261
283
|
});
|
|
262
284
|
this.nativeSyncQueue = sync.then(() => void 0, () => void 0);
|
|
263
285
|
return sync;
|
|
264
286
|
}
|
|
265
287
|
async backfillNativeStandardTools(native) {
|
|
266
|
-
const
|
|
288
|
+
const ownerWindow = this.ownerDocument?.defaultView;
|
|
289
|
+
const tools = (await native.getTools()).filter((tool) => {
|
|
290
|
+
if (tool.window === ownerWindow) return true;
|
|
291
|
+
if (ownerWindow?.parent !== ownerWindow) return false;
|
|
292
|
+
let frame = tool.window;
|
|
293
|
+
while (frame) {
|
|
294
|
+
if (frame === ownerWindow) return true;
|
|
295
|
+
const parent = frame.parent;
|
|
296
|
+
if (parent === frame) break;
|
|
297
|
+
frame = parent;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
});
|
|
267
301
|
if (this.closed) return;
|
|
302
|
+
for (const name of this.removingNativeTools) if (!tools.some((tool) => tool.name === name && tool.window === ownerWindow)) this.removingNativeTools.delete(name);
|
|
268
303
|
const nextTools = /* @__PURE__ */ new Map();
|
|
269
|
-
const nativeNames = new Set(tools.map(({ name }) => name));
|
|
270
|
-
for (const name of this.removingNativeTools) if (!nativeNames.has(name)) this.removingNativeTools.delete(name);
|
|
271
304
|
for (const tool of tools) {
|
|
272
305
|
if (nextTools.has(tool.name) || this.pendingTools.has(tool.name) || this.removingNativeTools.has(tool.name)) continue;
|
|
273
|
-
let inputSchema
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
} catch {
|
|
279
|
-
inputSchema = void 0;
|
|
280
|
-
}
|
|
281
|
-
if (!inputSchema) {
|
|
306
|
+
let inputSchema;
|
|
307
|
+
try {
|
|
308
|
+
inputSchema = JSON.parse(serializeInputSchema(tool.inputSchema ?? DEFAULT_INPUT_SCHEMA));
|
|
309
|
+
} catch {}
|
|
310
|
+
if (!isPlainObject(inputSchema)) {
|
|
282
311
|
console.warn(`[BrowserMcpServer] Native tool "${tool.name}" was not exposed over MCP because its input schema is malformed.`);
|
|
283
312
|
continue;
|
|
284
313
|
}
|
|
285
314
|
const item = {
|
|
286
315
|
name: tool.name,
|
|
287
|
-
...tool.title !== void 0 ? { title: tool.title } : {},
|
|
288
316
|
description: tool.description,
|
|
289
|
-
inputSchema
|
|
290
|
-
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
317
|
+
inputSchema
|
|
291
318
|
};
|
|
319
|
+
if (tool.title !== void 0) item.title = tool.title;
|
|
320
|
+
if (tool.annotations) item.annotations = tool.annotations;
|
|
292
321
|
nextTools.set(tool.name, {
|
|
293
322
|
source: tool,
|
|
294
323
|
item,
|
|
@@ -297,47 +326,41 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
297
326
|
}
|
|
298
327
|
for (const [name, current] of this.nativeBackfilledTools) {
|
|
299
328
|
const next = nextTools.get(name);
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
skipNative: true,
|
|
303
|
-
notify: false
|
|
304
|
-
});
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
this.nativeBackfilledTools.set(name, next);
|
|
329
|
+
if (next?.fingerprint === current.fingerprint) this.nativeBackfilledTools.set(name, next);
|
|
330
|
+
else this.removeTool(name, { skipNative: true });
|
|
308
331
|
}
|
|
309
332
|
for (const [name, next] of nextTools) {
|
|
310
333
|
if (this.tools.has(name)) continue;
|
|
311
|
-
const execute = async (args,
|
|
334
|
+
const execute = async (args, options) => {
|
|
312
335
|
const currentTool = this.nativeBackfilledTools.get(name)?.source;
|
|
313
336
|
if (!currentTool) throw new Error(`Native tool not found: ${name}`);
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
try {
|
|
338
|
+
return parseNativeToolResult(await native.executeTool(currentTool, args, options));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (currentTool.window.closed) this.nativeToolChangeListener();
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
316
343
|
};
|
|
317
344
|
const tool = {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
description: next.item.description,
|
|
321
|
-
inputSchema: next.item.inputSchema,
|
|
322
|
-
...next.item.annotations ? { annotations: next.item.annotations } : {},
|
|
323
|
-
execute: (args) => execute(args)
|
|
345
|
+
...next.item,
|
|
346
|
+
execute
|
|
324
347
|
};
|
|
325
348
|
try {
|
|
326
|
-
const normalized =
|
|
327
|
-
this.tools.set(
|
|
349
|
+
const normalized = normalizeInputSchema(tool.inputSchema);
|
|
350
|
+
this.tools.set(name, this.registerToolInMcp(tool, normalized, execute));
|
|
328
351
|
this.nativeBackfilledTools.set(name, next);
|
|
329
352
|
} catch (error) {
|
|
330
353
|
console.warn(`[BrowserMcpServer] Native tool "${name}" was not exposed over MCP because its schema could not be compiled:`, error);
|
|
331
354
|
}
|
|
332
355
|
}
|
|
356
|
+
this.applyToolExposure();
|
|
333
357
|
}
|
|
334
358
|
registerResource(descriptor) {
|
|
335
359
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
336
360
|
const registeredDescriptor = { ...descriptor };
|
|
337
|
-
const config = {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
};
|
|
361
|
+
const config = {};
|
|
362
|
+
if (registeredDescriptor.description !== void 0) config.description = registeredDescriptor.description;
|
|
363
|
+
if (registeredDescriptor.mimeType !== void 0) config.mimeType = registeredDescriptor.mimeType;
|
|
341
364
|
const template = registeredDescriptor.uri.includes("{") ? new ResourceTemplate(registeredDescriptor.uri, { list: void 0 }) : void 0;
|
|
342
365
|
const mcpHandle = template ? this.mcpServer.registerResource(registeredDescriptor.name, template, config, async (uri, variables) => registeredDescriptor.read(uri, variables)) : this.mcpServer.registerResource(registeredDescriptor.name, registeredDescriptor.uri, config, async (uri) => registeredDescriptor.read(uri));
|
|
343
366
|
this.registrations.add(mcpHandle);
|
|
@@ -347,15 +370,13 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
347
370
|
}
|
|
348
371
|
registerPrompt(descriptor) {
|
|
349
372
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
350
|
-
const registeredDescriptor = {
|
|
351
|
-
|
|
352
|
-
...descriptor.argsSchema === void 0 ? {} : { argsSchema: structuredClone(descriptor.argsSchema) }
|
|
353
|
-
};
|
|
373
|
+
const registeredDescriptor = { ...descriptor };
|
|
374
|
+
if (descriptor.argsSchema !== void 0) registeredDescriptor.argsSchema = structuredClone(descriptor.argsSchema);
|
|
354
375
|
const argsSchema = registeredDescriptor.argsSchema === void 0 ? void 0 : fromJsonSchema(registeredDescriptor.argsSchema);
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
const config = {};
|
|
377
|
+
if (registeredDescriptor.description !== void 0) config.description = registeredDescriptor.description;
|
|
378
|
+
if (argsSchema) config.argsSchema = argsSchema;
|
|
379
|
+
const mcpHandle = this.mcpServer.registerPrompt(registeredDescriptor.name, config, async (args) => registeredDescriptor.get(args ?? {}));
|
|
359
380
|
this.registrations.add(mcpHandle);
|
|
360
381
|
return { unregister: () => {
|
|
361
382
|
if (this.registrations.delete(mcpHandle)) mcpHandle.remove();
|
|
@@ -365,69 +386,28 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
365
386
|
return structuredClone([...this.tools.values()].map(({ item }) => item));
|
|
366
387
|
}
|
|
367
388
|
async getTools(options) {
|
|
368
|
-
validateWebMcpAccess(this.ownerDocument);
|
|
369
389
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
370
|
-
if (this.native)
|
|
371
|
-
|
|
372
|
-
if (options?.fromOrigins?.length) throw new DOMException("Cross-document tool discovery requires native WebMCP", "NotSupportedError");
|
|
373
|
-
const origin = globalThis.location?.origin ?? "";
|
|
374
|
-
const currentWindow = globalThis.window;
|
|
375
|
-
const tools = [...this.tools.values()].map(({ item, registeredInputSchema }) => {
|
|
376
|
-
const parsed = registeredInputSchema === void 0 ? void 0 : JSON.parse(registeredInputSchema);
|
|
377
|
-
return {
|
|
378
|
-
name: item.name,
|
|
379
|
-
title: item.title ?? "",
|
|
380
|
-
description: item.description,
|
|
381
|
-
...isPlainObject(parsed) ? { inputSchema: parsed } : {},
|
|
382
|
-
origin,
|
|
383
|
-
window: currentWindow,
|
|
384
|
-
...item.annotations ? { annotations: toWebMcpAnnotations(item.annotations) } : {}
|
|
385
|
-
};
|
|
386
|
-
}).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
387
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
388
|
-
return tools;
|
|
390
|
+
if (!this.native) throw createInvalidStateError("BrowserMcpServer has no WebMCP context");
|
|
391
|
+
return this.native.getTools(options);
|
|
389
392
|
}
|
|
390
|
-
async executeTool(
|
|
391
|
-
validateWebMcpAccess(this.ownerDocument);
|
|
393
|
+
async executeTool(...args) {
|
|
392
394
|
if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (tool === null || typeof tool !== "object") throw new TypeError("RegisteredTool must be an object");
|
|
396
|
-
for (const required of [
|
|
397
|
-
"name",
|
|
398
|
-
"description",
|
|
399
|
-
"window",
|
|
400
|
-
"origin"
|
|
401
|
-
]) if (!(required in tool)) throw new TypeError(`RegisteredTool.${required} is required`);
|
|
402
|
-
validateExecutableOrigin(tool.origin);
|
|
403
|
-
const origin = globalThis.location?.origin ?? "";
|
|
404
|
-
const currentWindow = globalThis.window;
|
|
405
|
-
if (tool.origin !== origin || tool.window !== currentWindow) throw createUnknownError(`Native tool not found: ${tool.name}`);
|
|
406
|
-
options?.signal?.throwIfAborted();
|
|
407
|
-
const registered = this.tools.get(tool.name);
|
|
408
|
-
if (!registered) throw createUnknownError(`Tool not found: ${tool.name}`);
|
|
409
|
-
const args = parseChromeToolInput(inputArgsJson);
|
|
410
|
-
try {
|
|
411
|
-
return serializeChromeToolResult(await withAbortSignal(registered.execute(args, options?.signal), registered.abortSignal, () => createUnknownError("Tool unregistered")));
|
|
412
|
-
} catch (error) {
|
|
413
|
-
if (options?.signal?.aborted && error === options.signal.reason) throw error;
|
|
414
|
-
if (registered.abortSignal?.aborted) throw error;
|
|
415
|
-
throw createToolInvocationFailedError(error);
|
|
416
|
-
}
|
|
395
|
+
if (!this.native) throw createInvalidStateError("BrowserMcpServer has no WebMCP context");
|
|
396
|
+
return this.native.executeTool(...args);
|
|
417
397
|
}
|
|
418
398
|
/**
|
|
419
399
|
* Re-evaluate which registered tools may appear on the wire.
|
|
420
400
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
401
|
+
* A published tool without `exposedTo` is enabled. `exposedTo` narrows a tool to named
|
|
402
|
+
* embedder origins. The peer origin is only known once a transport that reports one has
|
|
403
|
+
* heard from its peer, so a restricted tool stays disabled until then — and stays
|
|
404
|
+
* disabled forever on transports that cannot name a peer at all. Failing closed keeps a
|
|
405
|
+
* tool that asked for a narrow audience from reaching a wider one.
|
|
426
406
|
*/
|
|
427
407
|
applyToolExposure() {
|
|
428
408
|
for (const { exposedTo, mcpHandle } of this.tools.values()) {
|
|
429
|
-
if (!
|
|
430
|
-
const visible = this.peerOrigin !== void 0 && this.peerOrigin !== "null" && exposedTo.includes(this.peerOrigin);
|
|
409
|
+
if (!mcpHandle) continue;
|
|
410
|
+
const visible = !exposedTo?.length || this.peerOrigin !== void 0 && this.peerOrigin !== "null" && exposedTo.includes(this.peerOrigin);
|
|
431
411
|
if (visible === mcpHandle.enabled) continue;
|
|
432
412
|
if (visible) mcpHandle.enable();
|
|
433
413
|
else mcpHandle.disable();
|
|
@@ -444,7 +424,7 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
444
424
|
this.stopObservingPeerOrigin = void 0;
|
|
445
425
|
this.peerOrigin = void 0;
|
|
446
426
|
this.applyToolExposure();
|
|
447
|
-
if (!(
|
|
427
|
+
if (!isPeerOriginTransport(transport)) return;
|
|
448
428
|
const peered = transport;
|
|
449
429
|
this.peerOrigin = peered.clientOrigin;
|
|
450
430
|
this.applyToolExposure();
|
|
@@ -486,12 +466,11 @@ var BrowserMcpServer = class extends EventTarget {
|
|
|
486
466
|
this.stopObservingPeerOrigin?.();
|
|
487
467
|
this.stopObservingPeerOrigin = void 0;
|
|
488
468
|
this.closePromise = (async () => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
469
|
+
this.native?.removeEventListener("toolchange", this.nativeToolChangeListener);
|
|
470
|
+
this.native?.removeEventListener("toolactivated", this.nativeLifecycleListener);
|
|
471
|
+
this.native?.removeEventListener("toolcancel", this.nativeLifecycleListener);
|
|
493
472
|
for (const name of this.nativeToolAbortControllers.keys()) this.abortNativeToolMirror(name);
|
|
494
|
-
for (const name of this.tools.keys()) this.removeTool(name
|
|
473
|
+
for (const name of this.tools.keys()) this.removeTool(name);
|
|
495
474
|
for (const handle of this.registrations) handle.remove();
|
|
496
475
|
this.registrations.clear();
|
|
497
476
|
this.nativeBackfilledTools.clear();
|