@mcp-b/webmcp-ts-sdk 4.0.0 → 5.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/dist/index.js CHANGED
@@ -1,80 +1,13 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { mergeCapabilities, mergeCapabilities as mergeCapabilities$1 } from "@modelcontextprotocol/sdk/shared/protocol.js";
3
- import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
4
- import { CallToolRequestSchema, CallToolRequestSchema as CallToolRequestSchema$1, CallToolResultSchema, CreateMessageRequestSchema, CreateMessageResultSchema, ElicitRequestSchema, ElicitResultSchema, ErrorCode, GetPromptRequestSchema, GetPromptRequestSchema as GetPromptRequestSchema$1, GetPromptResultSchema, InitializeRequestSchema, InitializeResultSchema, JSONRPCMessageSchema, LATEST_PROTOCOL_VERSION, ListPromptsRequestSchema, ListPromptsRequestSchema as ListPromptsRequestSchema$1, ListPromptsResultSchema, ListResourcesRequestSchema, ListResourcesResultSchema, ListToolsRequestSchema, ListToolsRequestSchema as ListToolsRequestSchema$1, ListToolsResultSchema, LoggingLevelSchema, ReadResourceRequestSchema, ReadResourceResultSchema, ResourceListChangedNotificationSchema, SUPPORTED_PROTOCOL_VERSIONS, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
5
- import { toJsonValue } from "@mcp-b/webmcp-polyfill/schema";
6
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import { getParseErrorMessage, normalizeObjectSchema, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
8
- import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
9
- import { validateAndWarnToolName } from "@modelcontextprotocol/sdk/shared/toolNameValidation.js";
10
- import { CfWorkerJsonSchemaValidator, CfWorkerJsonSchemaValidator as CfWorkerJsonSchemaValidator$1, CfWorkerJsonSchemaValidator as PolyfillJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
1
+ import { coerceWebMcpToolDescriptor, createInvalidStateError, createToolInvocationFailedError, createUnknownError, isPlainObject, normalizeInputSchema, normalizeToolResponse, parseChromeToolInput, serializeChromeToolResult, toWebMcpAnnotations, validateExecutableOrigin, validatePotentiallyTrustworthyOrigins, validateWebMcpAccess, validateWebMcpToolDescriptor, withAbortSignal } from "@mcp-b/webmcp-polyfill/schema";
2
+ import { McpServer, ResourceTemplate, fromJsonSchema, isInputRequiredResult, mergeCapabilities } from "@modelcontextprotocol/server";
11
3
 
12
4
  //#region src/browser-server.ts
13
- const DEFAULT_INPUT_SCHEMA = {
14
- type: "object",
15
- properties: {}
16
- };
17
- const DEFAULT_CLIENT_REQUEST_TIMEOUT = 1e4;
5
+ const DEFAULT_INPUT_SCHEMA = normalizeInputSchema(void 0).inputSchema;
18
6
  const SERVER_MARKER_PROPERTY = "__isBrowserMcpServer";
19
- function isPlainObject(value) {
20
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
- }
22
- function isCallToolResult(value) {
23
- return isPlainObject(value) && Array.isArray(value.content);
24
- }
25
- function isPermissionsPolicySecurityError(error) {
26
- if (!error || typeof error !== "object") return false;
27
- const { name, message } = error;
28
- return name === "SecurityError" && typeof message === "string" && /permissions policy|feature "tools" is disallowed/i.test(message);
29
- }
30
- function isAbortError(error) {
31
- return Boolean(error) && typeof error === "object" && error.name === "AbortError";
32
- }
33
- function createAbortError() {
34
- if (typeof DOMException === "function") return new DOMException("signal is aborted without reason", "AbortError");
35
- const error = /* @__PURE__ */ new Error("signal is aborted without reason");
36
- error.name = "AbortError";
37
- return error;
38
- }
39
- function isPromiseLike(value) {
40
- return Boolean(value) && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
7
+ function isBrowserMcpServer(context) {
8
+ return typeof context === "object" && context !== null && SERVER_MARKER_PROPERTY in context && context[SERVER_MARKER_PROPERTY] === true;
41
9
  }
42
- function serializeTextContent(value) {
43
- if (typeof value === "string") return value;
44
- try {
45
- return JSON.stringify(value) ?? String(value);
46
- } catch {
47
- return String(value);
48
- }
49
- }
50
- function normalizeToolResponse(value) {
51
- if (isCallToolResult(value)) return value;
52
- const structuredContent = toJsonValue(value);
53
- return {
54
- content: [{
55
- type: "text",
56
- text: serializeTextContent(value)
57
- }],
58
- ...structuredContent !== void 0 ? { structuredContent } : {},
59
- isError: false
60
- };
61
- }
62
- function parseNativeToolInputSchema(inputSchema) {
63
- if (!inputSchema) return DEFAULT_INPUT_SCHEMA;
64
- try {
65
- const parsed = JSON.parse(inputSchema);
66
- if (isPlainObject(parsed)) return parsed;
67
- } catch {}
68
- return DEFAULT_INPUT_SCHEMA;
69
- }
70
- function toToolListItemFromNativeToolInfo(tool) {
71
- return {
72
- name: tool.name,
73
- description: tool.description ?? "",
74
- inputSchema: parseNativeToolInputSchema(tool.inputSchema)
75
- };
76
- }
77
- function parseNativeToolResult(toolName, serialized) {
10
+ function parseNativeToolResult(serialized) {
78
11
  if (serialized === null) return {
79
12
  content: [{
80
13
  type: "text",
@@ -82,609 +15,459 @@ function parseNativeToolResult(toolName, serialized) {
82
15
  }],
83
16
  isError: true
84
17
  };
85
- let parsed;
18
+ let result;
86
19
  try {
87
- parsed = JSON.parse(serialized);
88
- } catch (parseError) {
89
- throw new Error(`Failed to parse serialized tool response for ${toolName}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
20
+ result = JSON.parse(serialized);
21
+ } catch {
22
+ return normalizeToolResponse(serialized);
90
23
  }
91
- return normalizeToolResponse(parsed);
24
+ return isInputRequiredResult(result) ? result : normalizeToolResponse(result);
92
25
  }
93
- function withDefaultTimeout(options) {
94
- if (options?.signal) return options;
26
+ function toMcpInputSchema(normalized) {
27
+ const standardSchema = Object.getOwnPropertyDescriptor(normalized.inputSchema, "~standard");
28
+ return standardSchema && !standardSchema.enumerable ? normalized.inputSchema : fromJsonSchema(normalized.inputSchema);
29
+ }
30
+ function toMcpAnnotations(annotations) {
31
+ if (!annotations) return void 0;
32
+ const { untrustedContentHint: _untrustedContentHint, ...mcpAnnotations } = annotations;
33
+ return mcpAnnotations;
34
+ }
35
+ function toNativeTool(tool, inputSchema, execute) {
36
+ const annotations = tool.annotations ? toWebMcpAnnotations(tool.annotations) : void 0;
95
37
  return {
96
- ...options,
97
- signal: AbortSignal.timeout(DEFAULT_CLIENT_REQUEST_TIMEOUT)
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
98
44
  };
99
45
  }
100
46
  /**
101
- * Browser-optimized MCP Server that speaks WebMCP natively.
47
+ * Thin WebMCP-to-MCP v2 adapter.
102
48
  *
103
- * Implements `registerTool(tool, options?)` while retaining MCP capabilities
104
- * (resources, prompts, elicitation, sampling) via BaseMcpServer.
105
- *
106
- * Deprecated compatibility (kept for Chrome Beta 147 and existing wrappers,
107
- * removed in the next major): `unregisterTool(name)`.
108
- *
109
- * When `native` is provided, tool operations are mirrored so that
110
- * navigator.modelContextTesting stays in sync.
49
+ * The official v2 `McpServer` owns MCP registration, validation, and transport
50
+ * behavior. This class only adds the document-facing WebMCP contract and native
51
+ * Chrome mirroring.
111
52
  */
112
- var BrowserMcpServer = class extends McpServer {
53
+ var BrowserMcpServer = class extends EventTarget {
113
54
  [SERVER_MARKER_PROPERTY] = true;
55
+ mcpServer;
114
56
  native;
115
- _promptSchemas = /* @__PURE__ */ new Map();
116
- _jsonValidator;
117
- _publicMethodsBound = false;
118
- _unregisterToolDeprecationWarned = false;
119
- _nativeToolCleanups = /* @__PURE__ */ new Map();
120
- _tools = /* @__PURE__ */ new Map();
121
- _producerEventTarget = new EventTarget();
122
- _ontoolchange = null;
123
- _producerToolsChangedQueued = false;
124
- constructor(serverInfo, options) {
125
- const validator = options?.jsonSchemaValidator ?? new CfWorkerJsonSchemaValidator$1();
126
- const enhancedOptions = {
127
- capabilities: mergeCapabilities$1(options?.capabilities || {}, {
57
+ ownerDocument;
58
+ tools = /* @__PURE__ */ new Map();
59
+ peerOrigin;
60
+ registrations = /* @__PURE__ */ new Set();
61
+ nativeToolAbortControllers = /* @__PURE__ */ new Map();
62
+ nativeBackfilledTools = /* @__PURE__ */ new Map();
63
+ pendingTools = /* @__PURE__ */ new Map();
64
+ removingNativeTools = /* @__PURE__ */ new Set();
65
+ nativeSyncQueue = Promise.resolve();
66
+ nativeToolChangeQueue = Promise.resolve();
67
+ nativeToolChangeListener;
68
+ closed = false;
69
+ closePromise;
70
+ ontoolchangeHandler = null;
71
+ ontoolchangeListener = (event) => {
72
+ this.ontoolchangeHandler?.call(this, event);
73
+ };
74
+ constructor(serverInfo, options = {}) {
75
+ super();
76
+ const { native, ...serverOptions } = options;
77
+ this.mcpServer = new McpServer(serverInfo, {
78
+ ...serverOptions,
79
+ capabilities: mergeCapabilities(serverOptions.capabilities ?? {}, {
128
80
  tools: { listChanged: true },
129
81
  resources: { listChanged: true },
130
82
  prompts: { listChanged: true }
131
- }),
132
- jsonSchemaValidator: validator
133
- };
134
- super(serverInfo, enhancedOptions);
135
- this._jsonValidator = validator;
136
- this.native = options?.native;
137
- this.bindPublicApiMethods();
138
- this.installToolRequestHandlers();
139
- }
140
- /**
141
- * navigator.modelContext consumers may destructure methods (e.g. const { registerTool } = ...).
142
- * Bind methods once so they remain callable outside instance-method invocation syntax.
143
- */
144
- bindPublicApiMethods() {
145
- if (this._publicMethodsBound) return;
146
- this.registerTool = this.registerTool.bind(this);
147
- this.unregisterTool = this.unregisterTool.bind(this);
148
- this.listTools = this.listTools.bind(this);
149
- this.getTools = this.getTools.bind(this);
150
- this.callTool = this.callTool.bind(this);
151
- this.executeTool = this.executeTool.bind(this);
152
- this.addEventListener = this.addEventListener.bind(this);
153
- this.removeEventListener = this.removeEventListener.bind(this);
154
- this.dispatchEvent = this.dispatchEvent.bind(this);
155
- this.registerResource = this.registerResource.bind(this);
156
- this.listResources = this.listResources.bind(this);
157
- this.readResource = this.readResource.bind(this);
158
- this.registerPrompt = this.registerPrompt.bind(this);
159
- this.listPrompts = this.listPrompts.bind(this);
160
- this.getPrompt = this.getPrompt.bind(this);
161
- this.createMessage = this.createMessage.bind(this);
162
- this.elicitInput = this.elicitInput.bind(this);
163
- this._publicMethodsBound = true;
164
- }
165
- get _parentResources() {
166
- return this._registeredResources;
167
- }
168
- get _parentPrompts() {
169
- return this._registeredPrompts;
170
- }
171
- toJsonSchema(schema, pipeStrategy) {
172
- if (!schema || typeof schema !== "object") return {};
173
- const normalized = normalizeObjectSchema(schema);
174
- return normalized ? toJsonSchemaCompat(normalized, {
175
- strictUnions: true,
176
- pipeStrategy
177
- }) : schema;
83
+ })
84
+ });
85
+ this.native = native;
86
+ this.ownerDocument = globalThis.document ?? null;
87
+ if (native) {
88
+ this.nativeToolChangeListener = () => {
89
+ if (this.closed) return;
90
+ this.nativeToolChangeQueue = this.nativeToolChangeQueue.then(async () => {
91
+ try {
92
+ await this.syncNativeTools();
93
+ } catch (error) {
94
+ console.warn("[BrowserMcpServer] Native WebMCP tool reconciliation failed:", error);
95
+ }
96
+ await this.notifyProducerToolsChanged();
97
+ });
98
+ };
99
+ native.addEventListener("toolchange", this.nativeToolChangeListener);
100
+ }
178
101
  }
179
- toInputTransportSchema(schema) {
180
- const jsonSchema = this.toJsonSchema(schema, "input");
181
- if (Object.keys(jsonSchema).length === 0) return DEFAULT_INPUT_SCHEMA;
182
- if (jsonSchema.type === void 0) return {
183
- type: "object",
184
- ...jsonSchema
185
- };
186
- return jsonSchema;
102
+ get ontoolchange() {
103
+ return this.ontoolchangeHandler;
187
104
  }
188
- toOutputTransportSchema(schema) {
189
- return this.toJsonSchema(schema, "output");
105
+ set ontoolchange(handler) {
106
+ const listener = typeof handler === "function" ? handler : null;
107
+ if (listener === null) {
108
+ this.ontoolchangeHandler = null;
109
+ super.removeEventListener("toolchange", this.ontoolchangeListener);
110
+ return;
111
+ }
112
+ if (this.ontoolchangeHandler === null) super.addEventListener("toolchange", this.ontoolchangeListener);
113
+ this.ontoolchangeHandler = listener;
190
114
  }
191
- isZodSchema(schema) {
192
- if (!schema || typeof schema !== "object") return false;
193
- const s = schema;
194
- return "_zod" in s || "_def" in s;
195
- }
196
- getNativeLegacyToolsApi() {
197
- if (!this.native) return;
198
- const candidate = this.native;
199
- if (typeof candidate.listTools !== "function" || typeof candidate.callTool !== "function") return;
200
- return candidate;
115
+ async notifyProducerToolsChanged() {
116
+ if (this.closed) return;
117
+ await new Promise((resolve) => setTimeout(resolve, 0));
118
+ if (!this.closed) this.dispatchEvent(new Event("toolchange"));
201
119
  }
202
120
  getNativeStandardToolsApi() {
203
- if (!this.native) return;
204
121
  const candidate = this.native;
205
- if (typeof candidate.getTools !== "function" || typeof candidate.executeTool !== "function") return;
206
- return candidate;
207
- }
208
- getNativeUnregisterTool() {
209
- if (!this.native) return;
210
- const unregisterTool = this.native.unregisterTool;
211
- if (typeof unregisterTool !== "function") return;
212
- return (nameOrTool) => unregisterTool.call(this.native, nameOrTool);
213
- }
214
- createNativeToolCleanup(signal) {
215
- const controller = new AbortController();
216
- let removeSignalListener;
217
- if (signal) {
218
- const abortNativeSignal = () => {
219
- controller.abort();
220
- };
221
- if (signal.aborted) abortNativeSignal();
222
- else {
223
- signal.addEventListener("abort", abortNativeSignal, { once: true });
224
- removeSignalListener = () => signal.removeEventListener("abort", abortNativeSignal);
225
- }
226
- }
227
- return {
228
- options: { signal: controller.signal },
229
- abort: () => {
230
- removeSignalListener?.();
231
- if (!controller.signal.aborted) controller.abort();
232
- }
233
- };
122
+ return candidate && typeof candidate.executeTool === "function" ? candidate : void 0;
234
123
  }
235
- registerNativeToolMirror(tool, signal) {
124
+ async registerNativeToolMirror(tool, normalized, options, execute, controller) {
236
125
  if (!this.native) return;
237
126
  const nativeRegister = this.native.registerTool;
238
- const nativeUnregisterTool = this.getNativeUnregisterTool();
239
- const cleanup = Boolean(signal) || !nativeUnregisterTool ? this.createNativeToolCleanup(signal) : void 0;
240
- let nativeRegisterResult;
127
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
128
+ const nativeInputSchema = normalized.registeredInputSchema === void 0 ? void 0 : JSON.parse(normalized.registeredInputSchema);
129
+ this.nativeToolAbortControllers.set(tool.name, controller);
241
130
  try {
242
- if (cleanup) nativeRegisterResult = nativeRegister.call(this.native, tool, cleanup.options);
243
- else nativeRegisterResult = nativeRegister.call(this.native, tool);
131
+ await nativeRegister.call(this.native, toNativeTool(tool, nativeInputSchema, (input) => execute(input)), {
132
+ ...options,
133
+ signal
134
+ });
244
135
  } catch (error) {
245
- cleanup?.abort();
246
- if (isPermissionsPolicySecurityError(error)) {
247
- console.warn("[BrowserMcpServer] Native WebMCP tool mirror is blocked by permissions policy; continuing with WebMCP transport registration only.");
248
- return;
249
- }
136
+ controller.abort();
137
+ if (this.nativeToolAbortControllers.get(tool.name) === controller) this.nativeToolAbortControllers.delete(tool.name);
250
138
  throw error;
251
139
  }
252
- if (!cleanup) return;
253
- const nativeToolCleanup = {
254
- abort: cleanup.abort,
255
- nativeSignalAccepted: nativeRegister.length >= 2 || !nativeUnregisterTool
140
+ if (this.nativeToolAbortControllers.get(tool.name) === controller) this.removingNativeTools.delete(tool.name);
141
+ }
142
+ abortNativeToolMirror(name, controller = this.nativeToolAbortControllers.get(name)) {
143
+ if (!controller || this.nativeToolAbortControllers.get(name) !== controller) return;
144
+ this.removingNativeTools.add(name);
145
+ controller.abort();
146
+ this.nativeToolAbortControllers.delete(name);
147
+ }
148
+ validateToolDescriptor(tool) {
149
+ validateWebMcpToolDescriptor(tool);
150
+ if (this.tools.has(tool.name) || this.pendingTools.has(tool.name)) throw createInvalidStateError(`Tool already registered: ${tool.name}`);
151
+ return normalizeInputSchema(tool.inputSchema);
152
+ }
153
+ registerToolInMcp(tool, normalized, executeTool, exposedTo) {
154
+ const outputSchema = tool.outputSchema === void 0 ? void 0 : structuredClone(tool.outputSchema);
155
+ const mcpOutputSchema = outputSchema === void 0 ? void 0 : fromJsonSchema(outputSchema);
156
+ const mcpAnnotations = toMcpAnnotations(tool.annotations);
157
+ const mcpCompatibleInput = normalized.inputSchema.type === void 0 || normalized.inputSchema.type === "object";
158
+ const mcpHandle = mcpCompatibleInput ? this.mcpServer.registerTool(tool.name, {
159
+ ...tool.title !== void 0 ? { title: tool.title } : {},
160
+ description: tool.description,
161
+ inputSchema: toMcpInputSchema(normalized),
162
+ ...mcpOutputSchema ? { outputSchema: mcpOutputSchema } : {},
163
+ ...mcpAnnotations ? { annotations: mcpAnnotations } : {}
164
+ }, async (args, context) => {
165
+ const result = await executeTool(args, context.mcpReq.signal);
166
+ if (isInputRequiredResult(result)) throw new Error(`WebMCP tool "${tool.name}" returned input_required. Multi-round tool flows require BrowserMcpServer.mcpServer.registerTool().`);
167
+ return normalizeToolResponse(result);
168
+ }) : void 0;
169
+ if (exposedTo?.length) mcpHandle?.disable();
170
+ 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.`);
171
+ return {
172
+ item: {
173
+ name: tool.name,
174
+ ...tool.title !== void 0 ? { title: tool.title } : {},
175
+ description: tool.description,
176
+ inputSchema: normalized.inputSchema,
177
+ ...outputSchema === void 0 ? {} : { outputSchema },
178
+ ...tool.annotations ? { annotations: tool.annotations } : {}
179
+ },
180
+ ...normalized.registeredInputSchema !== void 0 ? { registeredInputSchema: normalized.registeredInputSchema } : {},
181
+ execute: executeTool,
182
+ mcpHandle,
183
+ ...exposedTo?.length ? { exposedTo } : {}
256
184
  };
257
- this._nativeToolCleanups.set(tool.name, nativeToolCleanup);
258
- if (isPromiseLike(nativeRegisterResult)) nativeRegisterResult.then(void 0, (error) => {
259
- cleanup.abort();
260
- if (this._nativeToolCleanups.get(tool.name) === nativeToolCleanup) this._nativeToolCleanups.delete(tool.name);
261
- if (isPermissionsPolicySecurityError(error)) {
262
- console.warn("[BrowserMcpServer] Native WebMCP tool mirror is blocked by permissions policy; continuing with WebMCP transport registration only.");
263
- return;
264
- }
265
- if (isAbortError(error)) return;
266
- console.warn("[BrowserMcpServer] Native WebMCP tool mirror registration rejected; continuing with WebMCP transport registration only.", error);
267
- });
268
- return nativeToolCleanup;
269
185
  }
270
- unregisterNativeToolMirror(name, options) {
271
- const cleanup = this._nativeToolCleanups.get(name);
272
- this._nativeToolCleanups.delete(name);
273
- const nativeUnregisterTool = this.getNativeUnregisterTool();
274
- if (options?.preferAbortSignal && cleanup?.nativeSignalAccepted === true || !nativeUnregisterTool) {
275
- cleanup?.abort();
276
- return;
277
- }
186
+ registerTool = async (toolValue, optionsValue = {}) => {
187
+ const options = optionsValue ?? {};
188
+ validateWebMcpAccess(this.ownerDocument);
189
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
190
+ if (!isPlainObject(toolValue)) throw new TypeError("registerTool(tool) requires a tool object");
191
+ const tool = coerceWebMcpToolDescriptor(toolValue);
192
+ const normalized = this.validateToolDescriptor(tool);
193
+ options.signal?.throwIfAborted();
194
+ validatePotentiallyTrustworthyOrigins(options.exposedTo);
195
+ options.signal?.throwIfAborted();
196
+ const execute = async (args, signal) => {
197
+ signal?.throwIfAborted();
198
+ return withAbortSignal(Promise.resolve().then(() => Reflect.apply(tool.execute, void 0, [args])), signal);
199
+ };
200
+ const controller = new AbortController();
201
+ let registered;
202
+ const abort = () => {
203
+ if (this.pendingTools.get(tool.name) === controller) this.pendingTools.delete(tool.name);
204
+ if (registered && this.tools.get(tool.name) === registered) this.removeTool(tool.name);
205
+ else this.abortNativeToolMirror(tool.name, controller);
206
+ };
207
+ options.signal?.addEventListener("abort", abort, { once: true });
208
+ this.pendingTools.set(tool.name, controller);
278
209
  try {
279
- nativeUnregisterTool(name);
210
+ await this.registerNativeToolMirror(tool, normalized, options, execute, controller);
211
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
212
+ options.signal?.throwIfAborted();
213
+ registered = this.registerToolInMcp(tool, normalized, execute, options.exposedTo);
214
+ this.tools.set(tool.name, registered);
215
+ this.applyToolExposure();
216
+ if (options.signal?.aborted) {
217
+ abort();
218
+ options.signal.throwIfAborted();
219
+ }
220
+ } catch (error) {
221
+ this.abortNativeToolMirror(tool.name, controller);
222
+ options.signal?.removeEventListener("abort", abort);
223
+ options.signal?.throwIfAborted();
224
+ throw error;
280
225
  } finally {
281
- cleanup?.abort();
226
+ if (this.pendingTools.get(tool.name) === controller) this.pendingTools.delete(tool.name);
227
+ }
228
+ if (options.signal) {
229
+ registered.abortSignal = options.signal;
230
+ registered.abortListener = abort;
231
+ }
232
+ if (this.tools.get(tool.name) === registered) if (this.native) await this.nativeToolChangeQueue;
233
+ else await this.notifyProducerToolsChanged();
234
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
235
+ options.signal?.throwIfAborted();
236
+ };
237
+ removeTool(name, options) {
238
+ const registered = this.tools.get(name);
239
+ if (registered) {
240
+ if (registered.abortSignal && registered.abortListener) registered.abortSignal.removeEventListener("abort", registered.abortListener);
241
+ this.tools.delete(name);
242
+ this.nativeBackfilledTools.delete(name);
243
+ registered.mcpHandle?.remove();
244
+ if ((options?.notify ?? true) && !this.native) this.notifyProducerToolsChanged();
282
245
  }
246
+ if (!options?.skipNative) this.abortNativeToolMirror(name);
283
247
  }
284
- installToolRequestHandlers() {
285
- this.server.setRequestHandler(ListToolsRequestSchema$1, () => ({ tools: this.listToolsForMcpTransport() }));
286
- this.server.setRequestHandler(CallToolRequestSchema$1, async (request) => {
287
- try {
288
- const params = request.params.arguments === void 0 ? { name: request.params.name } : {
289
- name: request.params.name,
290
- arguments: request.params.arguments
291
- };
292
- const result = await this.callTool(params);
293
- return this.toMcpCallToolResult(result);
294
- } catch (error) {
295
- return {
296
- content: [{
297
- type: "text",
298
- text: error instanceof Error ? error.message : String(error)
299
- }],
300
- isError: true
301
- };
302
- }
248
+ syncNativeTools() {
249
+ const sync = this.nativeSyncQueue.then(async () => {
250
+ if (this.closed) return;
251
+ const native = this.getNativeStandardToolsApi();
252
+ if (native) await this.backfillNativeStandardTools(native);
303
253
  });
304
- }
305
- registerToolInServer(tool) {
306
- validateAndWarnToolName(tool.name);
307
- const inputSchema = this.toInputTransportSchema(tool.inputSchema);
308
- const outputSchema = tool.outputSchema ? this.toOutputTransportSchema(tool.outputSchema) : void 0;
309
- if (this._tools.has(tool.name)) throw new Error(`Tool ${tool.name} is already registered`);
310
- const registeredTool = {
311
- ...tool.title ? { title: tool.title } : {},
312
- description: tool.description,
313
- inputSchema,
314
- ...outputSchema ? { outputSchema } : {},
315
- ...tool.annotations ? { annotations: tool.annotations } : {},
316
- handler: async (args) => {
317
- return normalizeToolResponse(await tool.execute(args, { requestUserInteraction: async (cb) => cb() }));
254
+ this.nativeSyncQueue = sync.then(() => void 0, () => void 0);
255
+ return sync;
256
+ }
257
+ async backfillNativeStandardTools(native) {
258
+ const tools = await native.getTools();
259
+ if (this.closed) return;
260
+ const nextTools = /* @__PURE__ */ new Map();
261
+ const nativeNames = new Set(tools.map(({ name }) => name));
262
+ for (const name of this.removingNativeTools) if (!nativeNames.has(name)) this.removingNativeTools.delete(name);
263
+ for (const tool of tools) {
264
+ if (nextTools.has(tool.name) || this.pendingTools.has(tool.name) || this.removingNativeTools.has(tool.name)) continue;
265
+ let inputSchema = DEFAULT_INPUT_SCHEMA;
266
+ if (tool.inputSchema !== void 0) try {
267
+ const serialized = typeof tool.inputSchema === "string" ? tool.inputSchema : JSON.stringify(tool.inputSchema);
268
+ const parsed = JSON.parse(serialized);
269
+ inputSchema = isPlainObject(parsed) ? parsed : void 0;
270
+ } catch {
271
+ inputSchema = void 0;
318
272
  }
319
- };
320
- this._tools.set(tool.name, registeredTool);
321
- this.sendToolListChanged();
322
- this.notifyProducerToolsChanged();
323
- }
324
- get ontoolchange() {
325
- return this._ontoolchange;
326
- }
327
- set ontoolchange(handler) {
328
- this._ontoolchange = handler;
329
- }
330
- addEventListener(type, listener, options) {
331
- this._producerEventTarget.addEventListener(type, listener, options);
332
- }
333
- removeEventListener(type, listener, options) {
334
- this._producerEventTarget.removeEventListener(type, listener, options);
335
- }
336
- dispatchEvent(event) {
337
- return this._producerEventTarget.dispatchEvent(event);
338
- }
339
- notifyProducerToolsChanged() {
340
- if (this._producerToolsChangedQueued) return;
341
- this._producerToolsChangedQueued = true;
342
- queueMicrotask(() => {
343
- this._producerToolsChangedQueued = false;
344
- const event = new Event("toolchange");
345
- try {
346
- this._ontoolchange?.call(this, event);
347
- } catch (error) {
348
- console.warn("[BrowserMcpServer] navigator.modelContext.ontoolchange handler threw:", error);
273
+ if (!inputSchema) {
274
+ console.warn(`[BrowserMcpServer] Native tool "${tool.name}" was not exposed over MCP because its input schema is malformed.`);
275
+ continue;
349
276
  }
350
- this.dispatchEvent(event);
351
- });
352
- }
353
- backfillTools(tools, execute) {
354
- let synced = 0;
355
- for (const sourceTool of tools) {
356
- if (!sourceTool?.name || this._tools.has(sourceTool.name)) continue;
357
- const toolDescriptor = {
358
- name: sourceTool.name,
359
- description: sourceTool.description ?? "",
360
- inputSchema: sourceTool.inputSchema ?? DEFAULT_INPUT_SCHEMA,
361
- execute: async (args) => execute(sourceTool.name, args)
277
+ const item = {
278
+ name: tool.name,
279
+ ...tool.title !== void 0 ? { title: tool.title } : {},
280
+ description: tool.description,
281
+ inputSchema,
282
+ ...tool.annotations ? { annotations: tool.annotations } : {}
362
283
  };
363
- if (sourceTool.outputSchema) toolDescriptor.outputSchema = sourceTool.outputSchema;
364
- if (sourceTool.annotations) toolDescriptor.annotations = sourceTool.annotations;
365
- this.registerToolInServer(toolDescriptor);
366
- synced++;
284
+ nextTools.set(tool.name, {
285
+ source: tool,
286
+ item,
287
+ fingerprint: JSON.stringify(item)
288
+ });
367
289
  }
368
- return synced;
369
- }
370
- registerTool(tool, options) {
371
- const signal = options?.signal;
372
- if (signal?.aborted) return Promise.reject(createAbortError());
373
- this.registerNativeToolMirror(tool, signal);
374
- try {
375
- this.registerToolInServer(tool);
376
- } catch (error) {
377
- if (this.native) try {
378
- this.unregisterNativeToolMirror(tool.name);
379
- } catch (rollbackError) {
380
- console.error("[BrowserMcpServer] Rollback of native tool registration failed:", rollbackError);
290
+ for (const [name, current] of this.nativeBackfilledTools) {
291
+ const next = nextTools.get(name);
292
+ if (!next || next.fingerprint !== current.fingerprint) {
293
+ this.removeTool(name, {
294
+ skipNative: true,
295
+ notify: false
296
+ });
297
+ continue;
381
298
  }
382
- throw error;
299
+ this.nativeBackfilledTools.set(name, next);
383
300
  }
384
- if (signal) signal.addEventListener("abort", () => {
385
- const removed = this._tools.delete(tool.name);
386
- if (removed) this.sendToolListChanged();
301
+ for (const [name, next] of nextTools) {
302
+ if (this.tools.has(name)) continue;
303
+ const execute = async (args, signal) => {
304
+ const currentTool = this.nativeBackfilledTools.get(name)?.source;
305
+ if (!currentTool) throw new Error(`Native tool not found: ${name}`);
306
+ const input = JSON.stringify(args);
307
+ return parseNativeToolResult(signal ? await native.executeTool.call(this.native, currentTool, input, { signal }) : await native.executeTool.call(this.native, currentTool, input));
308
+ };
309
+ const tool = {
310
+ name,
311
+ ...next.item.title !== void 0 ? { title: next.item.title } : {},
312
+ description: next.item.description,
313
+ inputSchema: next.item.inputSchema,
314
+ ...next.item.annotations ? { annotations: next.item.annotations } : {},
315
+ execute: (args) => execute(args)
316
+ };
387
317
  try {
388
- this.unregisterNativeToolMirror(tool.name, { preferAbortSignal: true });
318
+ const normalized = this.validateToolDescriptor(tool);
319
+ this.tools.set(tool.name, this.registerToolInMcp(tool, normalized, execute, void 0));
320
+ this.nativeBackfilledTools.set(name, next);
389
321
  } catch (error) {
390
- console.warn("[BrowserMcpServer] Native unregister via abort fallback failed:", error);
322
+ console.warn(`[BrowserMcpServer] Native tool "${name}" was not exposed over MCP because its schema could not be compiled:`, error);
391
323
  }
392
- if (removed) this.notifyProducerToolsChanged();
393
- }, { once: true });
394
- return Promise.resolve();
395
- }
396
- /**
397
- * Backfill tools that were already registered on the native/polyfill context
398
- * before this BrowserMcpServer wrapper was installed.
399
- */
400
- syncNativeTools() {
401
- let synced = 0;
402
- const nativeLegacyToolsApi = this.getNativeLegacyToolsApi();
403
- if (nativeLegacyToolsApi) {
404
- const nativeCallTool = nativeLegacyToolsApi.callTool.bind(nativeLegacyToolsApi);
405
- synced += this.backfillTools(nativeLegacyToolsApi.listTools(), async (name, args) => nativeCallTool({
406
- name,
407
- arguments: args
408
- }));
409
324
  }
410
- const nativeStandardToolsApi = this.getNativeStandardToolsApi();
411
- if (nativeStandardToolsApi) this.backfillNativeStandardTools(nativeStandardToolsApi).catch((error) => {
412
- console.warn("[BrowserMcpServer] Native WebMCP tool backfill failed:", error);
413
- });
414
- return synced;
415
- }
416
- async backfillNativeStandardTools(nativeToolsApi) {
417
- const nativeTools = await nativeToolsApi.getTools.call(this.native);
418
- return this.backfillTools(nativeTools.map(toToolListItemFromNativeToolInfo), async (name, args) => {
419
- const nativeTool = nativeTools.find((tool) => tool.name === name);
420
- if (!nativeTool) throw new Error(`Native tool not found: ${name}`);
421
- return parseNativeToolResult(name, await nativeToolsApi.executeTool.call(this.native, nativeTool, JSON.stringify(args ?? {})));
422
- });
423
- }
424
- unregisterTool(nameOrTool) {
425
- this.warnUnregisterToolDeprecationOnce();
426
- const name = this.resolveToolNameForUnregister(nameOrTool);
427
- const removed = this._tools.delete(name);
428
- if (removed) this.sendToolListChanged();
429
- if (this.native) this.unregisterNativeToolMirror(name);
430
- if (removed) this.notifyProducerToolsChanged();
431
325
  }
432
326
  registerResource(descriptor) {
433
- const registered = super.registerResource(descriptor.name, descriptor.uri, {
434
- ...descriptor.description !== void 0 && { description: descriptor.description },
435
- ...descriptor.mimeType !== void 0 && { mimeType: descriptor.mimeType }
436
- }, async (uri) => ({ contents: (await descriptor.read(uri)).contents }));
437
- return { unregister: () => registered.remove() };
327
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
328
+ const registeredDescriptor = { ...descriptor };
329
+ const config = {
330
+ ...registeredDescriptor.description !== void 0 ? { description: registeredDescriptor.description } : {},
331
+ ...registeredDescriptor.mimeType !== void 0 ? { mimeType: registeredDescriptor.mimeType } : {}
332
+ };
333
+ const template = registeredDescriptor.uri.includes("{") ? new ResourceTemplate(registeredDescriptor.uri, { list: void 0 }) : void 0;
334
+ 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));
335
+ this.registrations.add(mcpHandle);
336
+ return { unregister: () => {
337
+ if (this.registrations.delete(mcpHandle)) mcpHandle.remove();
338
+ } };
438
339
  }
439
340
  registerPrompt(descriptor) {
440
- if (descriptor.argsSchema) this._promptSchemas.set(descriptor.name, descriptor.argsSchema);
441
- const registered = super.registerPrompt(descriptor.name, { ...descriptor.description !== void 0 && { description: descriptor.description } }, async (args) => ({ messages: (await descriptor.get(args)).messages }));
341
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
342
+ const registeredDescriptor = {
343
+ ...descriptor,
344
+ ...descriptor.argsSchema === void 0 ? {} : { argsSchema: structuredClone(descriptor.argsSchema) }
345
+ };
346
+ const argsSchema = registeredDescriptor.argsSchema === void 0 ? void 0 : fromJsonSchema(registeredDescriptor.argsSchema);
347
+ const mcpHandle = this.mcpServer.registerPrompt(registeredDescriptor.name, {
348
+ ...registeredDescriptor.description !== void 0 ? { description: registeredDescriptor.description } : {},
349
+ ...argsSchema ? { argsSchema } : {}
350
+ }, async (args) => registeredDescriptor.get(args ?? {}));
351
+ this.registrations.add(mcpHandle);
442
352
  return { unregister: () => {
443
- this._promptSchemas.delete(descriptor.name);
444
- registered.remove();
353
+ if (this.registrations.delete(mcpHandle)) mcpHandle.remove();
445
354
  } };
446
355
  }
447
- resolveToolNameForUnregister(nameOrTool) {
448
- if (typeof nameOrTool === "string") return nameOrTool;
449
- if (isPlainObject(nameOrTool) && typeof nameOrTool.name === "string") return nameOrTool.name;
450
- throw new TypeError("Failed to execute 'unregisterTool' on 'ModelContext': parameter 1 must be a string or an object with a string name.");
451
- }
452
- warnUnregisterToolDeprecationOnce() {
453
- if (this._unregisterToolDeprecationWarned) return;
454
- this._unregisterToolDeprecationWarned = true;
455
- console.warn("[BrowserMcpServer] navigator.modelContext.unregisterTool() is deprecated. The April 23, 2026 WebMCP draft removed it in favor of registerTool(tool, { signal }) — pass an AbortSignal and abort it to unregister.");
456
- }
457
- listResources() {
458
- return Object.entries(this._parentResources).filter(([, resource]) => resource.enabled).map(([uri, resource]) => ({
459
- uri,
460
- name: resource.name,
461
- ...resource.metadata
462
- }));
463
- }
464
- async readResource(uri) {
465
- const resource = this._parentResources[uri];
466
- if (!resource) throw new Error(`Resource not found: ${uri}`);
467
- return resource.readCallback(new URL(uri), {});
468
- }
469
- listPrompts() {
470
- return Object.entries(this._parentPrompts).filter(([, prompt]) => prompt.enabled).map(([name, prompt]) => {
471
- const schema = this._promptSchemas.get(name);
472
- return {
473
- name,
474
- ...prompt.description !== void 0 && { description: prompt.description },
475
- ...schema?.properties ? { arguments: Object.entries(schema.properties).map(([argName, prop]) => ({
476
- name: argName,
477
- ...typeof prop === "object" && prop !== null && "description" in prop ? { description: prop.description } : {},
478
- ...schema.required?.includes(argName) ? { required: true } : {}
479
- })) } : {}
480
- };
481
- });
482
- }
483
- async getPrompt(name, args = {}) {
484
- const prompt = this._parentPrompts[name];
485
- if (!prompt) throw new Error(`Prompt not found: ${name}`);
486
- const schema = this._promptSchemas.get(name);
487
- if (schema) {
488
- const result = this._jsonValidator.getValidator(schema)(args);
489
- if (!result.valid) throw new Error(`Invalid arguments for prompt ${name}: ${result.errorMessage}`);
490
- }
491
- return prompt.callback(args, {});
492
- }
493
356
  listTools() {
494
- return [...this._tools.entries()].map(([name, tool]) => {
495
- const item = {
496
- name,
497
- description: tool.description ?? "",
498
- inputSchema: tool.inputSchema
499
- };
500
- if (tool.outputSchema) item.outputSchema = tool.outputSchema;
501
- if (tool.annotations) item.annotations = tool.annotations;
502
- return item;
503
- });
504
- }
505
- listToolsForMcpTransport() {
506
- return this.listTools().map((tool) => {
507
- if (!tool.outputSchema || tool.outputSchema.type === "object") return tool;
508
- if (tool.outputSchema.type === void 0) return {
509
- ...tool,
510
- outputSchema: {
511
- ...tool.outputSchema,
512
- type: "object"
513
- }
514
- };
515
- const { outputSchema: _outputSchema, ...mcpTool } = tool;
516
- return mcpTool;
517
- });
518
- }
519
- toMcpCallToolResult(result) {
520
- return {
521
- content: result.content,
522
- ...isPlainObject(result.structuredContent) ? { structuredContent: result.structuredContent } : {},
523
- ...result.isError !== void 0 ? { isError: result.isError } : {}
524
- };
525
- }
526
- async getTools() {
527
- const origin = typeof globalThis.location === "object" && typeof globalThis.location?.origin === "string" ? globalThis.location.origin : "";
528
- const currentWindow = typeof globalThis.window === "object" ? globalThis.window : void 0;
529
- return this.listTools().map((tool) => {
530
- let inputSchema;
531
- try {
532
- inputSchema = JSON.stringify(tool.inputSchema ?? DEFAULT_INPUT_SCHEMA);
533
- } catch {
534
- inputSchema = JSON.stringify(DEFAULT_INPUT_SCHEMA);
535
- }
357
+ return structuredClone([...this.tools.values()].map(({ item }) => item));
358
+ }
359
+ async getTools(options) {
360
+ validateWebMcpAccess(this.ownerDocument);
361
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
362
+ if (this.native) return this.native.getTools(options);
363
+ validatePotentiallyTrustworthyOrigins(options?.fromOrigins);
364
+ if (options?.fromOrigins?.length) throw new DOMException("Cross-document tool discovery requires native WebMCP", "NotSupportedError");
365
+ const origin = globalThis.location?.origin ?? "";
366
+ const currentWindow = globalThis.window;
367
+ const tools = [...this.tools.values()].map(({ item, registeredInputSchema }) => {
368
+ const parsed = registeredInputSchema === void 0 ? void 0 : JSON.parse(registeredInputSchema);
536
369
  return {
537
- name: tool.name,
538
- title: this._tools.get(tool.name)?.title ?? tool.description ?? "",
539
- description: tool.description ?? "",
540
- inputSchema,
370
+ name: item.name,
371
+ title: item.title ?? "",
372
+ description: item.description,
373
+ ...isPlainObject(parsed) ? { inputSchema: parsed } : {},
541
374
  origin,
542
- window: currentWindow
375
+ window: currentWindow,
376
+ ...item.annotations ? { annotations: toWebMcpAnnotations(item.annotations) } : {}
543
377
  };
544
- });
545
- }
546
- /**
547
- * Override SDK's validateToolInput to handle both Zod schemas and plain JSON Schema.
548
- * Zod schemas use the SDK's safeParseAsync; plain JSON Schema uses the configured SDK validator.
549
- */
550
- async validateToolInput(tool, args, toolName) {
551
- if (!tool.inputSchema) return void 0;
552
- if (this.isZodSchema(tool.inputSchema)) {
553
- const result = await safeParseAsync(tool.inputSchema, args ?? {});
554
- if (!result.success) throw new Error(`Invalid arguments for tool ${toolName}: ${getParseErrorMessage(result.error)}`);
555
- return result.data;
378
+ }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
379
+ await new Promise((resolve) => setTimeout(resolve, 0));
380
+ return tools;
381
+ }
382
+ async executeTool(tool, inputArgsJson, options) {
383
+ validateWebMcpAccess(this.ownerDocument);
384
+ if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
385
+ const native = this.getNativeStandardToolsApi();
386
+ if (native) return native.executeTool.call(this.native, tool, inputArgsJson, options);
387
+ if (tool === null || typeof tool !== "object") throw new TypeError("RegisteredTool must be an object");
388
+ for (const required of [
389
+ "name",
390
+ "description",
391
+ "window",
392
+ "origin"
393
+ ]) if (!(required in tool)) throw new TypeError(`RegisteredTool.${required} is required`);
394
+ validateExecutableOrigin(tool.origin);
395
+ const origin = globalThis.location?.origin ?? "";
396
+ const currentWindow = globalThis.window;
397
+ if (tool.origin !== origin || tool.window !== currentWindow) throw createUnknownError(`Native tool not found: ${tool.name}`);
398
+ options?.signal?.throwIfAborted();
399
+ const registered = this.tools.get(tool.name);
400
+ if (!registered) throw createUnknownError(`Tool not found: ${tool.name}`);
401
+ const args = parseChromeToolInput(inputArgsJson);
402
+ try {
403
+ return serializeChromeToolResult(await withAbortSignal(registered.execute(args, options?.signal), registered.abortSignal, () => createUnknownError("Tool unregistered")));
404
+ } catch (error) {
405
+ if (options?.signal?.aborted && error === options.signal.reason) throw error;
406
+ if (registered.abortSignal?.aborted) throw error;
407
+ throw createToolInvocationFailedError(error);
556
408
  }
557
- const result = this._jsonValidator.getValidator(tool.inputSchema)(args ?? {});
558
- if (!result.valid) throw new Error(`Invalid arguments for tool ${toolName}: ${result.errorMessage}`);
559
- return result.data;
560
409
  }
561
410
  /**
562
- * Override SDK's validateToolOutput to handle both Zod schemas and plain JSON Schema.
411
+ * Re-evaluate which registered tools may appear on the wire.
412
+ *
413
+ * `exposedTo` narrows a tool to named embedder origins. The peer origin is only known
414
+ * once a transport that reports one has heard from its peer, so a restricted tool stays
415
+ * disabled until then — and stays disabled forever on transports that cannot name a peer
416
+ * at all. Failing closed keeps a tool that asked for a narrow audience from reaching a
417
+ * wider one; unrestricted tools are untouched and behave exactly as before.
563
418
  */
564
- async validateToolOutput(tool, result, toolName) {
565
- if (!tool.outputSchema) return;
566
- const r = result;
567
- if (!("content" in r) || r.isError) return;
568
- if (r.structuredContent === void 0) throw new Error(`Output validation error: Tool ${toolName} has an output schema but no structured content was provided`);
569
- if (this.isZodSchema(tool.outputSchema)) {
570
- const parseResult = await safeParseAsync(tool.outputSchema, r.structuredContent);
571
- if (!parseResult.success) throw new Error(`Output validation error: Invalid structured content for tool ${toolName}: ${getParseErrorMessage(parseResult.error)}`);
572
- return;
419
+ applyToolExposure() {
420
+ for (const { exposedTo, mcpHandle } of this.tools.values()) {
421
+ if (!exposedTo?.length || !mcpHandle) continue;
422
+ const visible = this.peerOrigin !== void 0 && exposedTo.includes(this.peerOrigin);
423
+ if (visible === mcpHandle.enabled) continue;
424
+ if (visible) mcpHandle.enable();
425
+ else mcpHandle.disable();
573
426
  }
574
- const validationResult = this._jsonValidator.getValidator(tool.outputSchema)(r.structuredContent);
575
- if (!validationResult.valid) throw new Error(`Output validation error: Invalid structured content for tool ${toolName}: ${validationResult.errorMessage}`);
576
427
  }
577
428
  /**
578
- * Executes a registered tool by name.
579
- *
580
- * @deprecated Prefer the WebMCP standard producer path: get a descriptor from
581
- * `getTools()` and pass it to `executeTool(tool, inputArgsJson)`. This method
582
- * remains as an MCP-B compatibility convenience and MCP transport bridge.
429
+ * Follow the peer origin of transports that report one, so `exposedTo` can be scoped to
430
+ * the embedder actually connected. Duck-typed rather than imported: the SDK must not take
431
+ * a build dependency on `@mcp-b/transports`, and transports that never name a peer simply
432
+ * leave every restricted tool disabled.
583
433
  */
584
- async callTool(params) {
585
- const tool = this._tools.get(params.name);
586
- if (!tool) throw new Error(`Tool not found: ${params.name}`);
587
- const args = await this.validateToolInput(tool, params.arguments, params.name);
588
- const result = await tool.handler(args ?? {});
589
- await this.validateToolOutput(tool, result, params.name);
590
- return result;
591
- }
592
- async executeTool(toolOrName, inputArgsJsonOrArgs = {}) {
593
- if (typeof toolOrName === "string") return this.callTool({
594
- name: toolOrName,
595
- arguments: typeof inputArgsJsonOrArgs === "string" ? JSON.parse(inputArgsJsonOrArgs) : inputArgsJsonOrArgs
596
- });
597
- const result = await this.callTool({
598
- name: toolOrName.name,
599
- arguments: typeof inputArgsJsonOrArgs === "string" ? JSON.parse(inputArgsJsonOrArgs) : inputArgsJsonOrArgs
600
- });
601
- const serialized = JSON.stringify(result);
602
- return serialized === void 0 ? null : serialized;
434
+ observePeerOrigin(transport) {
435
+ if (!("clientOrigin" in transport)) return;
436
+ const peered = transport;
437
+ this.peerOrigin = peered.clientOrigin;
438
+ this.applyToolExposure();
439
+ const previous = peered.onclientorigin;
440
+ peered.onclientorigin = (origin) => {
441
+ previous?.(origin);
442
+ this.peerOrigin = origin;
443
+ this.applyToolExposure();
444
+ };
603
445
  }
604
- /**
605
- * Override connect to initialize request handlers BEFORE the transport connection.
606
- * This prevents "Cannot register capabilities after connecting to transport" errors
607
- * when tools are registered dynamically after connection.
608
- *
609
- * Tool handlers are installed with the public low-level Server API in the constructor.
610
- * Prompt/resource handlers still come from the parent, then prompt handlers are replaced
611
- * where the parent expects Zod shapes instead of plain JSON Schema.
612
- */
613
- async connect(transport) {
614
- this.setResourceRequestHandlers();
615
- this.setPromptRequestHandlers();
616
- this.server.setRequestHandler(ListPromptsRequestSchema$1, () => ({ prompts: this.listPrompts() }));
617
- this.server.setRequestHandler(GetPromptRequestSchema$1, async (request) => {
618
- const prompt = this._parentPrompts[request.params.name];
619
- if (!prompt) throw new Error(`Prompt ${request.params.name} not found`);
620
- if (!prompt.enabled) throw new Error(`Prompt ${request.params.name} disabled`);
621
- const schema = this._promptSchemas.get(request.params.name);
622
- if (schema) {
623
- const result = this._jsonValidator.getValidator(schema)(request.params.arguments ?? {});
624
- if (!result.valid) throw new Error(`Invalid arguments for prompt ${request.params.name}: ${result.errorMessage}`);
625
- return prompt.callback(request.params.arguments, {});
446
+ connect(transport) {
447
+ if (this.closed) return Promise.reject(createInvalidStateError("BrowserMcpServer is closed"));
448
+ this.observePeerOrigin(transport);
449
+ return this.mcpServer.connect(transport);
450
+ }
451
+ close() {
452
+ if (this.closePromise) return this.closePromise;
453
+ this.closed = true;
454
+ this.closePromise = (async () => {
455
+ if (this.native && this.nativeToolChangeListener) {
456
+ this.native.removeEventListener("toolchange", this.nativeToolChangeListener);
457
+ this.nativeToolChangeListener = void 0;
626
458
  }
627
- return prompt.callback({}, {});
628
- });
629
- return super.connect(transport);
630
- }
631
- async createMessage(params, options) {
632
- return this.server.createMessage(params, withDefaultTimeout(options));
633
- }
634
- async elicitInput(params, options) {
635
- return this.server.elicitInput(params, withDefaultTimeout(options));
636
- }
637
- };
638
-
639
- //#endregion
640
- //#region src/no-op-validator.ts
641
- /**
642
- * No-op JSON Schema validator for browser environments.
643
- *
644
- * This validator bypasses the MCP SDK's internal ajv-based validation which causes
645
- * "Error compiling schema" errors in browser extensions due to ajv's use of
646
- * eval/Function constructor.
647
- *
648
- * Validation is handled externally by Zod in @mcp-b/global, making the SDK's
649
- * internal validation redundant. This no-op validator allows the SDK to function
650
- * without the ajv dependency issues.
651
- */
652
- /**
653
- * A no-op JSON Schema validator that always returns valid.
654
- *
655
- * Use this in browser environments where:
656
- * - ajv causes "Error compiling schema" errors
657
- * - Validation is already handled elsewhere (e.g., by Zod)
658
- * - You need to avoid eval/Function constructor restrictions
659
- *
660
- * @example
661
- * ```typescript
662
- * import { BrowserMcpServer } from '@mcp-b/webmcp-ts-sdk';
663
- * import { NoOpJsonSchemaValidator } from '@mcp-b/webmcp-ts-sdk/no-op-validator';
664
- *
665
- * const server = new BrowserMcpServer(
666
- * { name: 'my-server', version: '1.0.0' },
667
- * { jsonSchemaValidator: new NoOpJsonSchemaValidator() }
668
- * );
669
- * ```
670
- */
671
- var NoOpJsonSchemaValidator = class {
672
- /**
673
- * Returns a validator function that always passes.
674
- * The input data is passed through unchanged.
675
- *
676
- * @param _schema - The JSON Schema (ignored)
677
- * @returns A validator function that always returns valid
678
- */
679
- getValidator(_schema) {
680
- return (input) => ({
681
- valid: true,
682
- data: input,
683
- errorMessage: void 0
684
- });
459
+ for (const name of this.nativeToolAbortControllers.keys()) this.abortNativeToolMirror(name);
460
+ for (const name of this.tools.keys()) this.removeTool(name, { notify: false });
461
+ for (const handle of this.registrations) handle.remove();
462
+ this.registrations.clear();
463
+ this.nativeBackfilledTools.clear();
464
+ this.removingNativeTools.clear();
465
+ await this.mcpServer.close();
466
+ })();
467
+ return this.closePromise;
685
468
  }
686
469
  };
687
470
 
688
471
  //#endregion
689
- export { BrowserMcpServer, BrowserMcpServer as McpServer, CallToolRequestSchema, CallToolResultSchema, CfWorkerJsonSchemaValidator, Client, CreateMessageRequestSchema, CreateMessageResultSchema, ElicitRequestSchema, ElicitResultSchema, ErrorCode, GetPromptRequestSchema, GetPromptResultSchema, InitializeRequestSchema, InitializeResultSchema, JSONRPCMessageSchema, LATEST_PROTOCOL_VERSION, ListPromptsRequestSchema, ListPromptsResultSchema, ListResourcesRequestSchema, ListResourcesResultSchema, ListToolsRequestSchema, ListToolsResultSchema, LoggingLevelSchema, NoOpJsonSchemaValidator, PolyfillJsonSchemaValidator, ReadBuffer, ReadResourceRequestSchema, ReadResourceResultSchema, ResourceListChangedNotificationSchema, SERVER_MARKER_PROPERTY, SUPPORTED_PROTOCOL_VERSIONS, ToolListChangedNotificationSchema, mergeCapabilities, serializeMessage };
472
+ export { BrowserMcpServer, isBrowserMcpServer };
690
473
  //# sourceMappingURL=index.js.map