@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/README.md +57 -210
- package/dist/index.d.ts +57 -205
- package/dist/index.js +392 -609
- package/dist/index.js.map +1 -1
- package/package.json +14 -25
- package/dist/index.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,80 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
20
|
-
return
|
|
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
|
|
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
|
|
18
|
+
let result;
|
|
86
19
|
try {
|
|
87
|
-
|
|
88
|
-
} catch
|
|
89
|
-
|
|
20
|
+
result = JSON.parse(serialized);
|
|
21
|
+
} catch {
|
|
22
|
+
return normalizeToolResponse(serialized);
|
|
90
23
|
}
|
|
91
|
-
return normalizeToolResponse(
|
|
24
|
+
return isInputRequiredResult(result) ? result : normalizeToolResponse(result);
|
|
92
25
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
*
|
|
47
|
+
* Thin WebMCP-to-MCP v2 adapter.
|
|
102
48
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
|
53
|
+
var BrowserMcpServer = class extends EventTarget {
|
|
113
54
|
[SERVER_MARKER_PROPERTY] = true;
|
|
55
|
+
mcpServer;
|
|
114
56
|
native;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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,
|
|
124
|
+
async registerNativeToolMirror(tool, normalized, options, execute, controller) {
|
|
236
125
|
if (!this.native) return;
|
|
237
126
|
const nativeRegister = this.native.registerTool;
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
131
|
+
await nativeRegister.call(this.native, toNativeTool(tool, nativeInputSchema, (input) => execute(input)), {
|
|
132
|
+
...options,
|
|
133
|
+
signal
|
|
134
|
+
});
|
|
244
135
|
} catch (error) {
|
|
245
|
-
|
|
246
|
-
if (
|
|
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 (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
271
|
-
const
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
this.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
if (this.
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
284
|
+
nextTools.set(tool.name, {
|
|
285
|
+
source: tool,
|
|
286
|
+
item,
|
|
287
|
+
fingerprint: JSON.stringify(item)
|
|
288
|
+
});
|
|
367
289
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
299
|
+
this.nativeBackfilledTools.set(name, next);
|
|
383
300
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 (
|
|
441
|
-
const
|
|
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.
|
|
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.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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:
|
|
538
|
-
title:
|
|
539
|
-
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (this.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
return this.
|
|
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,
|
|
472
|
+
export { BrowserMcpServer, isBrowserMcpServer };
|
|
690
473
|
//# sourceMappingURL=index.js.map
|