@mcp-web/core 0.1.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/LICENSE +201 -0
- package/README.md +253 -0
- package/dist/addTool.typetest.d.ts +11 -0
- package/dist/addTool.typetest.d.ts.map +1 -0
- package/dist/addTool.typetest.js +248 -0
- package/dist/create-state-tools.d.ts +77 -0
- package/dist/create-state-tools.d.ts.map +1 -0
- package/dist/create-state-tools.js +181 -0
- package/dist/create-tool.d.ts +90 -0
- package/dist/create-tool.d.ts.map +1 -0
- package/dist/create-tool.js +82 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.js +53 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts +2 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.js +331 -0
- package/dist/expanded-schema-tools/index.d.ts +4 -0
- package/dist/expanded-schema-tools/index.d.ts.map +1 -0
- package/dist/expanded-schema-tools/index.js +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/integration.test.js +599 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts +18 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.js +142 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.test.js +314 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts +69 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.js +139 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.test.js +223 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts +10 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.js +430 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts +2 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.test.js +689 -0
- package/dist/expanded-schema-tools/types.d.ts +26 -0
- package/dist/expanded-schema-tools/types.d.ts.map +1 -0
- package/dist/expanded-schema-tools/types.js +1 -0
- package/dist/expanded-schema-tools/utils.d.ts +16 -0
- package/dist/expanded-schema-tools/utils.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.js +35 -0
- package/dist/expanded-schema-tools/utils.test.d.ts +2 -0
- package/dist/expanded-schema-tools/utils.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.test.js +169 -0
- package/dist/group-state.d.ts +60 -0
- package/dist/group-state.d.ts.map +1 -0
- package/dist/group-state.js +54 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/query.d.ts +104 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +128 -0
- package/dist/schema-helpers.d.ts +69 -0
- package/dist/schema-helpers.d.ts.map +1 -0
- package/dist/schema-helpers.js +139 -0
- package/dist/schemas.d.ts +140 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +70 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts +23 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-basic-state-tools.js +95 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-fixed-shape-tools.js +53 -0
- package/dist/tool-generators/index.d.ts +6 -0
- package/dist/tool-generators/index.d.ts.map +1 -0
- package/dist/tool-generators/index.js +3 -0
- package/dist/tool-generators/schema-analysis.d.ts +18 -0
- package/dist/tool-generators/schema-analysis.d.ts.map +1 -0
- package/dist/tool-generators/schema-analysis.js +142 -0
- package/dist/tool-generators/schema-helpers.d.ts +87 -0
- package/dist/tool-generators/schema-helpers.d.ts.map +1 -0
- package/dist/tool-generators/schema-helpers.js +157 -0
- package/dist/tool-generators/tool-generator.d.ts +11 -0
- package/dist/tool-generators/tool-generator.d.ts.map +1 -0
- package/dist/tool-generators/tool-generator.js +437 -0
- package/dist/tool-generators/types.d.ts +26 -0
- package/dist/tool-generators/types.d.ts.map +1 -0
- package/dist/tool-generators/types.js +1 -0
- package/dist/tool-generators/utils.d.ts +16 -0
- package/dist/tool-generators/utils.d.ts.map +1 -0
- package/dist/tool-generators/utils.js +35 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +108 -0
- package/dist/web.d.ts +680 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1312 -0
- package/dist/zod-to-tools.d.ts +49 -0
- package/dist/zod-to-tools.d.ts.map +1 -0
- package/dist/zod-to-tools.js +623 -0
- package/package.json +58 -0
package/dist/web.js
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
import { decomposeSchema } from '@mcp-web/decompose-zod-schema';
|
|
2
|
+
import { AppDefinitionSchema, RESOURCE_MIME_TYPE, getDefaultAppResourceUri, getDefaultAppUrl, isCreatedApp, McpWebConfigSchema, QueryMessageSchema, ResourceDefinitionSchema, ToolDefinitionSchema, } from '@mcp-web/types';
|
|
3
|
+
import { ZodObject } from 'zod';
|
|
4
|
+
import { isCreatedStateTools } from './create-state-tools.js';
|
|
5
|
+
import { isCreatedTool } from './create-tool.js';
|
|
6
|
+
import { QueryResponse } from './query.js';
|
|
7
|
+
import { QueryRequestSchema, QueryResponseResultSchema } from './schemas.js';
|
|
8
|
+
import { generateBasicStateTools, generateToolsForSchema } from './tool-generators/index.js';
|
|
9
|
+
import { toJSONSchema, toToolMetadataJson } from './utils.js';
|
|
10
|
+
/**
|
|
11
|
+
* Main class for integrating web applications with AI agents via the Model Context Protocol (MCP).
|
|
12
|
+
*
|
|
13
|
+
* MCPWeb enables your web application to expose state and actions as tools that AI agents can
|
|
14
|
+
* interact with. It handles the WebSocket connection to the bridge server, tool registration,
|
|
15
|
+
* and bi-directional communication between your frontend and AI agents.
|
|
16
|
+
*
|
|
17
|
+
* @example Basic Usage
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { MCPWeb } from '@mcp-web/core';
|
|
20
|
+
*
|
|
21
|
+
* const mcp = new MCPWeb({
|
|
22
|
+
* name: 'My Todo App',
|
|
23
|
+
* description: 'A todo application that AI agents can control',
|
|
24
|
+
* autoConnect: true,
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Register a tool
|
|
28
|
+
* mcp.addTool({
|
|
29
|
+
* name: 'create_todo',
|
|
30
|
+
* description: 'Create a new todo item',
|
|
31
|
+
* handler: (input) => {
|
|
32
|
+
* const todo = { id: crypto.randomUUID(), ...input };
|
|
33
|
+
* todos.push(todo);
|
|
34
|
+
* return todo;
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example With Full Configuration
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const mcp = new MCPWeb({
|
|
42
|
+
* name: 'Checkers Game',
|
|
43
|
+
* description: 'Interactive checkers game controllable by AI agents',
|
|
44
|
+
* bridgeUrl: 'localhost:3001',
|
|
45
|
+
* icon: 'https://example.com/icon.png',
|
|
46
|
+
* agentUrl: 'localhost:3003',
|
|
47
|
+
* autoConnect: true,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export class MCPWeb {
|
|
52
|
+
#ws = null;
|
|
53
|
+
#sessionId;
|
|
54
|
+
#authToken;
|
|
55
|
+
#tools = new Map();
|
|
56
|
+
#resources = new Map();
|
|
57
|
+
#apps = new Map();
|
|
58
|
+
#connected = false;
|
|
59
|
+
#isConnecting = false;
|
|
60
|
+
#authError = null;
|
|
61
|
+
#config;
|
|
62
|
+
#toolRegistrationErrorCallbacks = new Map();
|
|
63
|
+
#mcpConfig;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a new MCPWeb instance with the specified configuration.
|
|
66
|
+
*
|
|
67
|
+
* The constructor initializes the WebSocket connection settings, generates or loads
|
|
68
|
+
* authentication credentials, and optionally auto-connects to the bridge server.
|
|
69
|
+
*
|
|
70
|
+
* @param config - Configuration object for MCPWeb
|
|
71
|
+
* @throws {Error} If configuration validation fails
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const mcp = new MCPWeb({
|
|
76
|
+
* name: 'My Todo App',
|
|
77
|
+
* description: 'A todo application that AI agents can control',
|
|
78
|
+
* bridgeUrl: 'localhost:3001',
|
|
79
|
+
* autoConnect: true,
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
constructor(config) {
|
|
84
|
+
this.#config = McpWebConfigSchema.parse(config);
|
|
85
|
+
this.#sessionId = this.#generateSessionId();
|
|
86
|
+
this.#authToken = this.#resolveAuthToken(this.#config);
|
|
87
|
+
this.#mcpConfig = {
|
|
88
|
+
[this.#config.name]: {
|
|
89
|
+
command: 'npx',
|
|
90
|
+
args: ['@mcp-web/client'],
|
|
91
|
+
env: {
|
|
92
|
+
MCP_SERVER_URL: `${this.#getHttpProtocol()}//${this.#config.bridgeUrl}`,
|
|
93
|
+
AUTH_TOKEN: this.#authToken
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
if (this.#config.autoConnect) {
|
|
98
|
+
this.connect();
|
|
99
|
+
}
|
|
100
|
+
this.setupActivityTracking();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Unique session identifier for this frontend instance.
|
|
104
|
+
*
|
|
105
|
+
* The session ID is automatically generated on construction.
|
|
106
|
+
* It's used to identify this specific frontend instance in the bridge server.
|
|
107
|
+
*
|
|
108
|
+
* @returns The session ID string
|
|
109
|
+
*/
|
|
110
|
+
get sessionId() {
|
|
111
|
+
return this.#sessionId;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Authentication token for this session.
|
|
115
|
+
*
|
|
116
|
+
* The auth token is either auto-generated, loaded from localStorage, or provided via config.
|
|
117
|
+
* By default, it's persisted in localStorage to maintain the same token across page reloads.
|
|
118
|
+
*
|
|
119
|
+
* @returns The authentication token string
|
|
120
|
+
*/
|
|
121
|
+
get authToken() {
|
|
122
|
+
return this.#authToken;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Map of all registered tools.
|
|
126
|
+
*
|
|
127
|
+
* Provides access to the internal tool registry. Each tool is keyed by its name.
|
|
128
|
+
*
|
|
129
|
+
* @returns Map of tool names to processed tool definitions
|
|
130
|
+
*/
|
|
131
|
+
get tools() {
|
|
132
|
+
return this.#tools;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Map of all registered resources.
|
|
136
|
+
*
|
|
137
|
+
* Provides access to the internal resource registry. Each resource is keyed by its URI.
|
|
138
|
+
*
|
|
139
|
+
* @returns Map of resource URIs to resource definitions
|
|
140
|
+
*/
|
|
141
|
+
get resources() {
|
|
142
|
+
return this.#resources;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Map of all registered MCP Apps.
|
|
146
|
+
*
|
|
147
|
+
* Provides access to the internal app registry. Each app is keyed by its name.
|
|
148
|
+
*
|
|
149
|
+
* @returns Map of app names to processed app definitions
|
|
150
|
+
*/
|
|
151
|
+
get apps() {
|
|
152
|
+
return this.#apps;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The processed MCPWeb configuration.
|
|
156
|
+
*
|
|
157
|
+
* Returns the validated and processed configuration with all defaults applied.
|
|
158
|
+
*
|
|
159
|
+
* @returns The complete configuration object
|
|
160
|
+
*/
|
|
161
|
+
get config() {
|
|
162
|
+
return this.#config;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Configuration object for the AI host app (e.g., Claude Desktop) using stdio transport.
|
|
166
|
+
*
|
|
167
|
+
* Use this to configure the MCP client in your AI host application.
|
|
168
|
+
* It contains the connection details and authentication credentials needed
|
|
169
|
+
* for the AI agent to connect to the bridge server via the `@mcp-web/client` stdio wrapper.
|
|
170
|
+
*
|
|
171
|
+
* For a simpler configuration, consider using `remoteMcpConfig` instead, which
|
|
172
|
+
* uses Remote MCP (Streamable HTTP) and doesn't require an intermediate process.
|
|
173
|
+
*
|
|
174
|
+
* @returns MCP client configuration object for stdio transport
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* console.log('Add this to your Claude Desktop config:');
|
|
179
|
+
* console.log(JSON.stringify(mcp.mcpConfig, null, 2));
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
get mcpConfig() {
|
|
183
|
+
return this.#mcpConfig;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Configuration object for the AI host app (e.g., Claude Desktop) using Remote MCP.
|
|
187
|
+
*
|
|
188
|
+
* This is the recommended configuration method. It uses Remote MCP (Streamable HTTP)
|
|
189
|
+
* to connect directly to the bridge server via URL, without needing an intermediate
|
|
190
|
+
* stdio process like `@mcp-web/client`.
|
|
191
|
+
*
|
|
192
|
+
* @returns MCP client configuration object for Remote MCP (URL-based)
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* console.log('Add this to your Claude Desktop config:');
|
|
197
|
+
* console.log(JSON.stringify(mcp.remoteMcpConfig, null, 2));
|
|
198
|
+
* // Output:
|
|
199
|
+
* // {
|
|
200
|
+
* // "my-app": {
|
|
201
|
+
* // "url": "https://localhost:3001?token=your-auth-token"
|
|
202
|
+
* // }
|
|
203
|
+
* // }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
get remoteMcpConfig() {
|
|
207
|
+
return {
|
|
208
|
+
[this.#config.name]: {
|
|
209
|
+
url: `${this.#getHttpProtocol()}//${this.#config.bridgeUrl}?token=${this.#authToken}`,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
#getBridgeWsUrl() {
|
|
214
|
+
return `${this.#getWsProtocol()}//${this.#config.bridgeUrl}`;
|
|
215
|
+
}
|
|
216
|
+
#getWsProtocol() {
|
|
217
|
+
return globalThis.window?.location?.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
218
|
+
}
|
|
219
|
+
#getHttpProtocol() {
|
|
220
|
+
return globalThis.window?.location?.protocol === 'https:' ? 'https:' : 'http:';
|
|
221
|
+
}
|
|
222
|
+
#generateSessionId() {
|
|
223
|
+
return globalThis.crypto.randomUUID();
|
|
224
|
+
}
|
|
225
|
+
#generateAuthToken() {
|
|
226
|
+
return globalThis.crypto.randomUUID();
|
|
227
|
+
}
|
|
228
|
+
#loadAuthToken() {
|
|
229
|
+
try {
|
|
230
|
+
return globalThis.localStorage?.getItem('mcp-web-auth-token');
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.warn('Failed to load auth token from localStorage:', error);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
#saveAuthToken(token) {
|
|
238
|
+
try {
|
|
239
|
+
globalThis.localStorage?.setItem('mcp-web-auth-token', token);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.warn('Failed to save auth token to localStorage:', error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
#resolveAuthToken(config) {
|
|
246
|
+
// Use provided auth token if available
|
|
247
|
+
if (config.authToken) {
|
|
248
|
+
// Save it to localStorage if persistence is enabled
|
|
249
|
+
if (globalThis.localStorage && config.persistAuthToken !== false) {
|
|
250
|
+
this.#saveAuthToken(config.authToken);
|
|
251
|
+
}
|
|
252
|
+
return config.authToken;
|
|
253
|
+
}
|
|
254
|
+
// Try to load from localStorage if persistence is enabled
|
|
255
|
+
if (globalThis.localStorage && config.persistAuthToken !== false) {
|
|
256
|
+
const savedToken = this.#loadAuthToken();
|
|
257
|
+
if (savedToken) {
|
|
258
|
+
return savedToken;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Generate new token
|
|
262
|
+
const newToken = this.#generateAuthToken();
|
|
263
|
+
// Save it if persistence is enabled
|
|
264
|
+
if (config.persistAuthToken !== false) {
|
|
265
|
+
this.#saveAuthToken(newToken);
|
|
266
|
+
}
|
|
267
|
+
return newToken;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Establishes connection to the bridge server.
|
|
271
|
+
*
|
|
272
|
+
* Opens a WebSocket connection to the bridge server and authenticates using the session's
|
|
273
|
+
* auth token. If `autoConnect` is enabled in the config, this is called automatically
|
|
274
|
+
* during construction.
|
|
275
|
+
*
|
|
276
|
+
* This method is idempotent - calling it multiple times while already connected or
|
|
277
|
+
* connecting will return the same promise.
|
|
278
|
+
*
|
|
279
|
+
* @returns Promise that resolves to `true` when authenticated and ready
|
|
280
|
+
* @throws {Error} If WebSocket connection fails
|
|
281
|
+
*
|
|
282
|
+
* @example Manual Connection
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const mcp = new MCPWeb({
|
|
285
|
+
* name: 'My App',
|
|
286
|
+
* description: 'My application',
|
|
287
|
+
* autoConnect: false, // Disable auto-connect
|
|
288
|
+
* });
|
|
289
|
+
*
|
|
290
|
+
* // Connect when ready
|
|
291
|
+
* await mcp.connect();
|
|
292
|
+
* console.log('Connected to bridge');
|
|
293
|
+
* ```
|
|
294
|
+
*
|
|
295
|
+
* @example Check Connection Status
|
|
296
|
+
* ```typescript
|
|
297
|
+
* if (!mcp.connected) {
|
|
298
|
+
* await mcp.connect();
|
|
299
|
+
* }
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
async connect() {
|
|
303
|
+
// Prevent duplicate connections
|
|
304
|
+
if (this.#connected) {
|
|
305
|
+
return Promise.resolve(true);
|
|
306
|
+
}
|
|
307
|
+
if (this.#isConnecting) {
|
|
308
|
+
// Return a promise that resolves when the current connection completes
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
const checkConnection = () => {
|
|
311
|
+
if (this.#connected) {
|
|
312
|
+
resolve(true);
|
|
313
|
+
}
|
|
314
|
+
else if (!this.#isConnecting) {
|
|
315
|
+
// Connection failed, try again
|
|
316
|
+
this.connect().then(resolve);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
setTimeout(checkConnection, 100);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
checkConnection();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
this.#isConnecting = true;
|
|
326
|
+
this.#authError = null;
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
try {
|
|
329
|
+
const wsUrl = `${this.#getBridgeWsUrl()}?session=${this.sessionId}`;
|
|
330
|
+
this.#ws = new WebSocket(wsUrl);
|
|
331
|
+
this.#ws.onopen = () => {
|
|
332
|
+
// Use setTimeout to ensure WebSocket is fully ready
|
|
333
|
+
setTimeout(() => this.authenticate(), 0);
|
|
334
|
+
};
|
|
335
|
+
this.#ws.onmessage = (event) => {
|
|
336
|
+
try {
|
|
337
|
+
const message = JSON.parse(event.data);
|
|
338
|
+
this.handleMessage(message);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.error('Failed to parse message:', error);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
this.#ws.onclose = () => {
|
|
345
|
+
this.#connected = false;
|
|
346
|
+
this.#isConnecting = false;
|
|
347
|
+
// Don't reconnect if authentication was rejected
|
|
348
|
+
if (!this.#authError) {
|
|
349
|
+
this.scheduleReconnect();
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
this.#ws.onerror = (error) => {
|
|
353
|
+
console.error('WebSocket error:', error);
|
|
354
|
+
this.#isConnecting = false;
|
|
355
|
+
reject(error);
|
|
356
|
+
};
|
|
357
|
+
// Resolve when authenticated, reject on auth failure
|
|
358
|
+
const checkConnection = () => {
|
|
359
|
+
if (this.#connected) {
|
|
360
|
+
this.#isConnecting = false;
|
|
361
|
+
resolve(true);
|
|
362
|
+
}
|
|
363
|
+
else if (this.#authError) {
|
|
364
|
+
this.#isConnecting = false;
|
|
365
|
+
reject(new Error(this.#authError.error));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
setTimeout(checkConnection, 100);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
setTimeout(checkConnection, 100);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
this.#isConnecting = false;
|
|
375
|
+
reject(error);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
authenticate() {
|
|
380
|
+
if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN)
|
|
381
|
+
return;
|
|
382
|
+
const authMessage = {
|
|
383
|
+
type: 'authenticate',
|
|
384
|
+
sessionId: this.sessionId,
|
|
385
|
+
authToken: this.#authToken,
|
|
386
|
+
origin: globalThis.window?.location?.origin,
|
|
387
|
+
pageTitle: globalThis.document?.title,
|
|
388
|
+
sessionName: this.#config.sessionName,
|
|
389
|
+
userAgent: globalThis.navigator?.userAgent,
|
|
390
|
+
timestamp: Date.now()
|
|
391
|
+
};
|
|
392
|
+
this.#ws.send(JSON.stringify(authMessage));
|
|
393
|
+
}
|
|
394
|
+
handleMessage(message) {
|
|
395
|
+
switch (message.type) {
|
|
396
|
+
case 'authenticated':
|
|
397
|
+
this.#connected = true;
|
|
398
|
+
this.registerAllTools();
|
|
399
|
+
this.registerAllResources();
|
|
400
|
+
break;
|
|
401
|
+
case 'authentication-failed': {
|
|
402
|
+
const failedMessage = message;
|
|
403
|
+
this.#authError = {
|
|
404
|
+
error: failedMessage.error,
|
|
405
|
+
code: failedMessage.code,
|
|
406
|
+
};
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case 'tool-call':
|
|
410
|
+
this.handleToolCall(message);
|
|
411
|
+
break;
|
|
412
|
+
case 'resource-read':
|
|
413
|
+
this.handleResourceRead(message);
|
|
414
|
+
break;
|
|
415
|
+
case 'tool-registration-error':
|
|
416
|
+
this.handleToolRegistrationError(message);
|
|
417
|
+
break;
|
|
418
|
+
case 'query_accepted':
|
|
419
|
+
case 'query_progress':
|
|
420
|
+
case 'query_failure':
|
|
421
|
+
case 'query_complete':
|
|
422
|
+
case 'query_cancel':
|
|
423
|
+
// Query responses are handled by query-specific listeners in query() method
|
|
424
|
+
// (see line ~498 where addEventListener('message') is set up per query)
|
|
425
|
+
break;
|
|
426
|
+
default:
|
|
427
|
+
console.warn('Unknown message type:', message.type);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async handleToolCall(message) {
|
|
431
|
+
const { requestId, toolName, toolInput } = message;
|
|
432
|
+
try {
|
|
433
|
+
const tool = this.tools.get(toolName);
|
|
434
|
+
if (!tool) {
|
|
435
|
+
this.sendToolResponse(requestId, { error: `Tool '${toolName}' not found` });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (tool.inputZodSchema) {
|
|
439
|
+
const parsedToolInput = tool.inputZodSchema.safeParse(toolInput);
|
|
440
|
+
if (!parsedToolInput.success) {
|
|
441
|
+
this.sendToolResponse(requestId, { error: `Invalid input: ${parsedToolInput.error.message}` });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Execute the tool
|
|
446
|
+
const result = await tool.handler(toolInput);
|
|
447
|
+
// Validate output if Zod schema is provided
|
|
448
|
+
if (tool.outputZodSchema) {
|
|
449
|
+
const parsedResult = tool.outputZodSchema.safeParse(result);
|
|
450
|
+
if (!parsedResult.success) {
|
|
451
|
+
this.sendToolResponse(requestId, { error: `Invalid output: ${parsedResult.error.message}` });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Send the raw result - the bridge will handle formatting
|
|
456
|
+
this.sendToolResponse(requestId, result);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
// Only log in non-test environments
|
|
460
|
+
if (globalThis.process?.env?.NODE_ENV !== 'test') {
|
|
461
|
+
console.debug(`Tool execution error for ${toolName}:`, error);
|
|
462
|
+
}
|
|
463
|
+
this.sendToolResponse(requestId, {
|
|
464
|
+
error: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
sendToolResponse(requestId, result) {
|
|
469
|
+
if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
|
|
470
|
+
console.error('Cannot send tool response: WebSocket not connected');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const response = {
|
|
474
|
+
type: 'tool-response',
|
|
475
|
+
requestId,
|
|
476
|
+
result
|
|
477
|
+
};
|
|
478
|
+
this.#ws.send(JSON.stringify(response));
|
|
479
|
+
}
|
|
480
|
+
registerAllTools() {
|
|
481
|
+
this.tools.forEach((tool) => {
|
|
482
|
+
this.registerTool(tool);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
registerTool(tool) {
|
|
486
|
+
if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const message = {
|
|
490
|
+
type: 'register-tool',
|
|
491
|
+
tool: {
|
|
492
|
+
name: tool.name,
|
|
493
|
+
description: tool.description,
|
|
494
|
+
inputSchema: tool.inputJsonSchema,
|
|
495
|
+
outputSchema: tool.outputJsonSchema,
|
|
496
|
+
// Forward _meta (e.g., _meta.ui.resourceUri for MCP Apps)
|
|
497
|
+
...(tool._meta ? { _meta: tool._meta } : {}),
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
this.#ws.send(JSON.stringify(message));
|
|
501
|
+
}
|
|
502
|
+
registerAllResources() {
|
|
503
|
+
this.resources.forEach((resource) => {
|
|
504
|
+
this.registerResource(resource);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
registerResource(resource) {
|
|
508
|
+
if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const message = {
|
|
512
|
+
type: 'register-resource',
|
|
513
|
+
resource: {
|
|
514
|
+
uri: resource.uri,
|
|
515
|
+
name: resource.name,
|
|
516
|
+
description: resource.description,
|
|
517
|
+
mimeType: resource.mimeType ?? 'text/html',
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
this.#ws.send(JSON.stringify(message));
|
|
521
|
+
}
|
|
522
|
+
async handleResourceRead(message) {
|
|
523
|
+
const { requestId, uri } = message;
|
|
524
|
+
try {
|
|
525
|
+
const resource = this.resources.get(uri);
|
|
526
|
+
if (!resource) {
|
|
527
|
+
this.sendResourceResponse(requestId, {
|
|
528
|
+
error: `Resource '${uri}' not found`,
|
|
529
|
+
mimeType: 'text/plain',
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const content = await resource.handler();
|
|
534
|
+
const mimeType = resource.mimeType ?? 'text/html';
|
|
535
|
+
if (content instanceof Uint8Array) {
|
|
536
|
+
const base64 = btoa(String.fromCharCode(...content));
|
|
537
|
+
this.sendResourceResponse(requestId, { blob: base64, mimeType });
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
this.sendResourceResponse(requestId, { content, mimeType });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
if (globalThis.process?.env?.NODE_ENV !== 'test') {
|
|
545
|
+
console.debug(`Resource read error for ${uri}:`, error);
|
|
546
|
+
}
|
|
547
|
+
this.sendResourceResponse(requestId, {
|
|
548
|
+
error: `Resource read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
549
|
+
mimeType: 'text/plain',
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
sendResourceResponse(requestId, result) {
|
|
554
|
+
if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
|
|
555
|
+
console.error('Cannot send resource response: WebSocket not connected');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const response = {
|
|
559
|
+
type: 'resource-response',
|
|
560
|
+
requestId,
|
|
561
|
+
...result,
|
|
562
|
+
};
|
|
563
|
+
this.#ws.send(JSON.stringify(response));
|
|
564
|
+
}
|
|
565
|
+
handleToolRegistrationError(message) {
|
|
566
|
+
const { toolName, error, message: errorMessage } = message;
|
|
567
|
+
// Remove the tool since the bridge rejected it
|
|
568
|
+
this.tools.delete(toolName);
|
|
569
|
+
// Call the per-tool callback if registered
|
|
570
|
+
const callback = this.#toolRegistrationErrorCallbacks.get(toolName);
|
|
571
|
+
if (callback) {
|
|
572
|
+
this.#toolRegistrationErrorCallbacks.delete(toolName);
|
|
573
|
+
callback({ toolName, code: error, message: errorMessage });
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
// No callback registered — log a warning so the error isn't silently lost
|
|
577
|
+
console.warn(`Tool registration rejected by bridge: '${toolName}' — ${errorMessage}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
scheduleReconnect() {
|
|
581
|
+
if (!this.#ws)
|
|
582
|
+
return;
|
|
583
|
+
setTimeout(() => {
|
|
584
|
+
if (!this.#connected && this.#ws) {
|
|
585
|
+
this.connect().catch(console.error);
|
|
586
|
+
}
|
|
587
|
+
}, 5000);
|
|
588
|
+
}
|
|
589
|
+
setupActivityTracking() {
|
|
590
|
+
const updateActivity = () => {
|
|
591
|
+
if (this.#ws && this.#ws.readyState === WebSocket.OPEN) {
|
|
592
|
+
this.#ws.send(JSON.stringify({
|
|
593
|
+
type: 'activity',
|
|
594
|
+
timestamp: Date.now()
|
|
595
|
+
}));
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
// Track user activity (only in browser environment)
|
|
599
|
+
if (globalThis.document) {
|
|
600
|
+
['click', 'scroll', 'keydown', 'mousemove'].forEach(event => {
|
|
601
|
+
globalThis.document.addEventListener(event, updateActivity, { passive: true });
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
// Periodic activity update
|
|
605
|
+
setInterval(updateActivity, 30000); // Every 30 seconds
|
|
606
|
+
}
|
|
607
|
+
addTool(tool, options) {
|
|
608
|
+
// Handle CreatedTool
|
|
609
|
+
if (isCreatedTool(tool)) {
|
|
610
|
+
return this.addTool(tool.definition, options);
|
|
611
|
+
}
|
|
612
|
+
const validationResult = ToolDefinitionSchema.safeParse(tool);
|
|
613
|
+
if (!validationResult.success) {
|
|
614
|
+
throw new Error(`Invalid tool definition: ${validationResult.error.message}`);
|
|
615
|
+
}
|
|
616
|
+
const isInputZodSchema = validationResult.data.inputSchema && 'safeParse' in validationResult.data.inputSchema;
|
|
617
|
+
const isOutputZodSchema = validationResult.data.outputSchema && 'safeParse' in validationResult.data.outputSchema;
|
|
618
|
+
// Create processed tool with both Zod and JSON schemas
|
|
619
|
+
const processedTool = {
|
|
620
|
+
...validationResult.data,
|
|
621
|
+
// Preserve _meta from original input (not in Zod schema)
|
|
622
|
+
_meta: tool._meta,
|
|
623
|
+
inputZodSchema: isInputZodSchema ? validationResult.data.inputSchema : undefined,
|
|
624
|
+
outputZodSchema: isOutputZodSchema ? validationResult.data.outputSchema : undefined,
|
|
625
|
+
inputJsonSchema: validationResult.data.inputSchema ? toJSONSchema(validationResult.data.inputSchema) : undefined,
|
|
626
|
+
outputJsonSchema: validationResult.data.outputSchema ? toJSONSchema(validationResult.data.outputSchema) : undefined
|
|
627
|
+
};
|
|
628
|
+
// Wrap handler with input validation if Zod schema is provided
|
|
629
|
+
if (processedTool.inputZodSchema) {
|
|
630
|
+
const originalHandler = processedTool.handler;
|
|
631
|
+
processedTool.handler = async (input) => {
|
|
632
|
+
const validationResult = processedTool.inputZodSchema?.safeParse(input);
|
|
633
|
+
if (!validationResult?.success) {
|
|
634
|
+
throw new Error(`Invalid tool input: ${validationResult?.error?.message}`);
|
|
635
|
+
}
|
|
636
|
+
return originalHandler(validationResult?.data);
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
this.tools.set(processedTool.name, processedTool);
|
|
640
|
+
// Store registration error callback if provided
|
|
641
|
+
if (options?.onRegistrationError) {
|
|
642
|
+
this.#toolRegistrationErrorCallbacks.set(processedTool.name, options.onRegistrationError);
|
|
643
|
+
}
|
|
644
|
+
// Register immediately if connected
|
|
645
|
+
if (this.#connected) {
|
|
646
|
+
this.registerTool(processedTool);
|
|
647
|
+
}
|
|
648
|
+
return tool;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Removes a registered tool.
|
|
652
|
+
*
|
|
653
|
+
* After removal, AI agents will no longer be able to call this tool.
|
|
654
|
+
* Useful for dynamically disabling features or cleaning up when tools are no longer needed.
|
|
655
|
+
*
|
|
656
|
+
* @param name - Name of the tool to remove
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```typescript
|
|
660
|
+
* // Remove a specific tool
|
|
661
|
+
* mcp.removeTool('create_todo');
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
removeTool(name) {
|
|
665
|
+
this.tools.delete(name);
|
|
666
|
+
this.#toolRegistrationErrorCallbacks.delete(name);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Registers a resource that AI agents can read.
|
|
670
|
+
*
|
|
671
|
+
* Resources are content that AI agents can request, such as HTML for MCP Apps.
|
|
672
|
+
* The handler function is called when the AI requests the resource content.
|
|
673
|
+
*
|
|
674
|
+
* @param resource - Resource configuration including URI, name, description, and handler
|
|
675
|
+
* @returns The registered resource definition
|
|
676
|
+
* @throws {Error} If resource definition is invalid
|
|
677
|
+
*
|
|
678
|
+
* @example Basic Resource
|
|
679
|
+
* ```typescript
|
|
680
|
+
* mcp.addResource({
|
|
681
|
+
* uri: 'ui://my-app/statistics.html',
|
|
682
|
+
* name: 'Statistics View',
|
|
683
|
+
* description: 'Statistics visualization for the app',
|
|
684
|
+
* mimeType: 'text/html',
|
|
685
|
+
* handler: async () => {
|
|
686
|
+
* const response = await fetch('/mcp-web-apps/statistics.html');
|
|
687
|
+
* return response.text();
|
|
688
|
+
* },
|
|
689
|
+
* });
|
|
690
|
+
* ```
|
|
691
|
+
*
|
|
692
|
+
* @example Resource from URL
|
|
693
|
+
* ```typescript
|
|
694
|
+
* mcp.addResource({
|
|
695
|
+
* uri: 'ui://my-app/chart.html',
|
|
696
|
+
* name: 'Chart Component',
|
|
697
|
+
* handler: () => fetch('/components/chart.html').then(r => r.text()),
|
|
698
|
+
* });
|
|
699
|
+
* ```
|
|
700
|
+
*/
|
|
701
|
+
addResource(resource) {
|
|
702
|
+
const validationResult = ResourceDefinitionSchema.safeParse(resource);
|
|
703
|
+
if (!validationResult.success) {
|
|
704
|
+
throw new Error(`Invalid resource definition: ${validationResult.error.message}`);
|
|
705
|
+
}
|
|
706
|
+
this.#resources.set(resource.uri, resource);
|
|
707
|
+
// Register immediately if connected
|
|
708
|
+
if (this.#connected) {
|
|
709
|
+
this.registerResource(resource);
|
|
710
|
+
}
|
|
711
|
+
return resource;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Removes a registered resource.
|
|
715
|
+
*
|
|
716
|
+
* After removal, AI agents will no longer be able to read this resource.
|
|
717
|
+
*
|
|
718
|
+
* @param uri - URI of the resource to remove
|
|
719
|
+
*
|
|
720
|
+
* @example
|
|
721
|
+
* ```typescript
|
|
722
|
+
* mcp.removeResource('ui://my-app/statistics.html');
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
removeResource(uri) {
|
|
726
|
+
this.#resources.delete(uri);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Registers an MCP App that AI agents can invoke to show visual UI.
|
|
730
|
+
*
|
|
731
|
+
* An MCP App combines a tool (that AI calls to get props) with a resource (the HTML UI).
|
|
732
|
+
* When AI calls the tool, the handler returns props. The tool response includes
|
|
733
|
+
* `_meta.ui.resourceUri` which tells the host to fetch and render the app HTML.
|
|
734
|
+
*
|
|
735
|
+
* @param app - App configuration including name, description, handler, and optional URL
|
|
736
|
+
* @returns The registered app definition
|
|
737
|
+
* @throws {Error} If app definition is invalid
|
|
738
|
+
*
|
|
739
|
+
* @example Basic App
|
|
740
|
+
* ```typescript
|
|
741
|
+
* mcp.addApp({
|
|
742
|
+
* name: 'show_statistics',
|
|
743
|
+
* description: 'Display statistics visualization',
|
|
744
|
+
* handler: () => ({
|
|
745
|
+
* completionRate: 0.75,
|
|
746
|
+
* totalTasks: 100,
|
|
747
|
+
* completedTasks: 75,
|
|
748
|
+
* }),
|
|
749
|
+
* });
|
|
750
|
+
* ```
|
|
751
|
+
*
|
|
752
|
+
* @example With Input Schema
|
|
753
|
+
* ```typescript
|
|
754
|
+
* mcp.addApp({
|
|
755
|
+
* name: 'show_chart',
|
|
756
|
+
* description: 'Display a chart with the given data',
|
|
757
|
+
* inputSchema: z.object({
|
|
758
|
+
* chartType: z.enum(['bar', 'line', 'pie']).describe('Type of chart'),
|
|
759
|
+
* title: z.string().describe('Chart title'),
|
|
760
|
+
* }),
|
|
761
|
+
* handler: ({ chartType, title }) => ({
|
|
762
|
+
* chartType,
|
|
763
|
+
* title,
|
|
764
|
+
* data: getChartData(),
|
|
765
|
+
* }),
|
|
766
|
+
* });
|
|
767
|
+
* ```
|
|
768
|
+
*
|
|
769
|
+
* @example With Custom URL
|
|
770
|
+
* ```typescript
|
|
771
|
+
* mcp.addApp({
|
|
772
|
+
* name: 'show_dashboard',
|
|
773
|
+
* description: 'Display the main dashboard',
|
|
774
|
+
* handler: () => getDashboardData(),
|
|
775
|
+
* url: '/custom/path/dashboard.html',
|
|
776
|
+
* });
|
|
777
|
+
* ```
|
|
778
|
+
*
|
|
779
|
+
* @example With Pre-Created App
|
|
780
|
+
* ```typescript
|
|
781
|
+
* import { createApp } from '@mcp-web/app';
|
|
782
|
+
*
|
|
783
|
+
* const statisticsApp = createApp({
|
|
784
|
+
* name: 'show_statistics',
|
|
785
|
+
* description: 'Display statistics',
|
|
786
|
+
* handler: () => ({ rate: 0.75 }),
|
|
787
|
+
* });
|
|
788
|
+
*
|
|
789
|
+
* mcp.addApp(statisticsApp);
|
|
790
|
+
* ```
|
|
791
|
+
*/
|
|
792
|
+
addApp(app) {
|
|
793
|
+
// Handle CreatedApp
|
|
794
|
+
if (isCreatedApp(app)) {
|
|
795
|
+
return this.addApp(app.definition);
|
|
796
|
+
}
|
|
797
|
+
const validationResult = AppDefinitionSchema.safeParse(app);
|
|
798
|
+
if (!validationResult.success) {
|
|
799
|
+
throw new Error(`Invalid app definition: ${validationResult.error.message}`);
|
|
800
|
+
}
|
|
801
|
+
// Process the app with resolved defaults
|
|
802
|
+
const resolvedUrl = app.url ?? getDefaultAppUrl(app.name);
|
|
803
|
+
const resolvedResourceUri = app.resourceUri ?? getDefaultAppResourceUri(app.name);
|
|
804
|
+
const processedApp = {
|
|
805
|
+
...app,
|
|
806
|
+
resolvedUrl,
|
|
807
|
+
resolvedResourceUri,
|
|
808
|
+
};
|
|
809
|
+
this.#apps.set(app.name, processedApp);
|
|
810
|
+
// Create and register the tool that wraps the handler
|
|
811
|
+
const toolHandler = async (input) => {
|
|
812
|
+
// Call the app's handler to get props
|
|
813
|
+
const props = await processedApp.handler(input);
|
|
814
|
+
// Return props with _meta.ui for MCP Apps protocol
|
|
815
|
+
return {
|
|
816
|
+
...props,
|
|
817
|
+
_meta: {
|
|
818
|
+
ui: {
|
|
819
|
+
resourceUri: resolvedResourceUri,
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
// Use the JSON Schema overload - schemas will be converted internally
|
|
825
|
+
this.addTool({
|
|
826
|
+
name: app.name,
|
|
827
|
+
description: app.description,
|
|
828
|
+
handler: toolHandler,
|
|
829
|
+
inputSchema: app.inputSchema
|
|
830
|
+
? toJSONSchema(app.inputSchema)
|
|
831
|
+
: undefined,
|
|
832
|
+
outputSchema: app.propsSchema
|
|
833
|
+
? toJSONSchema(app.propsSchema)
|
|
834
|
+
: undefined,
|
|
835
|
+
// Include _meta.ui in tool description (per ext-apps spec)
|
|
836
|
+
// so the host knows this tool has a UI before calling it
|
|
837
|
+
_meta: {
|
|
838
|
+
ui: {
|
|
839
|
+
resourceUri: resolvedResourceUri,
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
// Create and register the resource that serves the app HTML
|
|
844
|
+
this.addResource({
|
|
845
|
+
uri: resolvedResourceUri,
|
|
846
|
+
name: `${app.name} UI`,
|
|
847
|
+
description: `HTML UI for ${app.name}`,
|
|
848
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
849
|
+
handler: async () => {
|
|
850
|
+
// Fetch the HTML from the URL
|
|
851
|
+
const response = await fetch(resolvedUrl);
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
throw new Error(`Failed to fetch app HTML from ${resolvedUrl}: ${response.status}`);
|
|
854
|
+
}
|
|
855
|
+
return response.text();
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
return app;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Removes a registered MCP App.
|
|
862
|
+
*
|
|
863
|
+
* After removal, AI agents will no longer be able to invoke this app.
|
|
864
|
+
* This also removes the associated tool and resource.
|
|
865
|
+
*
|
|
866
|
+
* @param name - Name of the app to remove
|
|
867
|
+
*
|
|
868
|
+
* @example
|
|
869
|
+
* ```typescript
|
|
870
|
+
* mcp.removeApp('show_statistics');
|
|
871
|
+
* ```
|
|
872
|
+
*/
|
|
873
|
+
removeApp(name) {
|
|
874
|
+
const app = this.#apps.get(name);
|
|
875
|
+
if (app) {
|
|
876
|
+
// Remove the tool
|
|
877
|
+
this.removeTool(name);
|
|
878
|
+
// Remove the resource
|
|
879
|
+
this.removeResource(app.resolvedResourceUri);
|
|
880
|
+
// Remove from apps map
|
|
881
|
+
this.#apps.delete(name);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Implementation
|
|
885
|
+
addStateTools(optionsOrCreated) {
|
|
886
|
+
// Handle CreatedStateTools
|
|
887
|
+
if (isCreatedStateTools(optionsOrCreated)) {
|
|
888
|
+
const created = optionsOrCreated;
|
|
889
|
+
const registeredTools = [];
|
|
890
|
+
// Register all tools
|
|
891
|
+
for (const tool of created.tools) {
|
|
892
|
+
registeredTools.push(this.addTool(tool));
|
|
893
|
+
}
|
|
894
|
+
// Build cleanup function
|
|
895
|
+
const cleanup = () => {
|
|
896
|
+
for (const tool of registeredTools) {
|
|
897
|
+
this.removeTool(tool.name);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
// Return tuple based on isExpanded
|
|
901
|
+
if (created.isExpanded) {
|
|
902
|
+
return [registeredTools[0], registeredTools.slice(1), cleanup];
|
|
903
|
+
}
|
|
904
|
+
return [registeredTools[0], registeredTools[1], cleanup];
|
|
905
|
+
}
|
|
906
|
+
// At this point, optionsOrCreated is guaranteed to be the config object (not CreatedStateTools)
|
|
907
|
+
const options = optionsOrCreated;
|
|
908
|
+
const { name, description, get, set, schema, schemaSplit, expand } = options;
|
|
909
|
+
const allTools = [];
|
|
910
|
+
// Determine if we're expanding (schemaSplit or expand flag)
|
|
911
|
+
const isZodObjectSchema = schema instanceof ZodObject;
|
|
912
|
+
const shouldDecompose = schemaSplit && isZodObjectSchema;
|
|
913
|
+
// Step 1: Apply schemaSplit if provided
|
|
914
|
+
let decomposedSchemas = [];
|
|
915
|
+
if (shouldDecompose) {
|
|
916
|
+
decomposedSchemas = decomposeSchema(schema, schemaSplit);
|
|
917
|
+
}
|
|
918
|
+
// Step 2: Generate tools based on mode
|
|
919
|
+
if (expand) {
|
|
920
|
+
// Expanded mode: generate targeted tools for collections
|
|
921
|
+
if (decomposedSchemas.length > 0) {
|
|
922
|
+
// Generate expanded tools for each decomposed part
|
|
923
|
+
for (const decomposed of decomposedSchemas) {
|
|
924
|
+
const result = generateToolsForSchema({
|
|
925
|
+
name: `${name}_${decomposed.name}`,
|
|
926
|
+
description: `${decomposed.name} in ${description}`,
|
|
927
|
+
get: () => {
|
|
928
|
+
const fullState = get();
|
|
929
|
+
const extracted = {};
|
|
930
|
+
for (const path of decomposed.targetPaths) {
|
|
931
|
+
extracted[path] = fullState[path];
|
|
932
|
+
}
|
|
933
|
+
return Object.keys(extracted).length === 1
|
|
934
|
+
? extracted[decomposed.targetPaths[0]]
|
|
935
|
+
: extracted;
|
|
936
|
+
},
|
|
937
|
+
set: (value) => {
|
|
938
|
+
const current = get();
|
|
939
|
+
if (decomposed.targetPaths.length === 1) {
|
|
940
|
+
set({ ...current, [decomposed.targetPaths[0]]: value });
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
set({ ...current, ...value });
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
schema: decomposed.schema,
|
|
947
|
+
});
|
|
948
|
+
// Register and collect tools
|
|
949
|
+
for (const tool of result.tools) {
|
|
950
|
+
allTools.push(this.addTool(tool));
|
|
951
|
+
}
|
|
952
|
+
// Log warnings if any
|
|
953
|
+
for (const warning of result.warnings) {
|
|
954
|
+
console.warn(warning);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
// Generate expanded tools for full schema
|
|
960
|
+
const result = generateToolsForSchema({
|
|
961
|
+
name,
|
|
962
|
+
description,
|
|
963
|
+
get: get,
|
|
964
|
+
set: set,
|
|
965
|
+
schema: schema,
|
|
966
|
+
});
|
|
967
|
+
// Register and collect tools
|
|
968
|
+
for (const tool of result.tools) {
|
|
969
|
+
allTools.push(this.addTool(tool));
|
|
970
|
+
}
|
|
971
|
+
// Log warnings if any
|
|
972
|
+
for (const warning of result.warnings) {
|
|
973
|
+
console.warn(warning);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
else if (shouldDecompose) {
|
|
978
|
+
// Decompose mode without expand: use basic state tools with decomposition
|
|
979
|
+
const basicResult = generateBasicStateTools({
|
|
980
|
+
name,
|
|
981
|
+
description,
|
|
982
|
+
get,
|
|
983
|
+
set,
|
|
984
|
+
schema: schema,
|
|
985
|
+
schemaSplit,
|
|
986
|
+
});
|
|
987
|
+
// Register getter
|
|
988
|
+
allTools.push(this.addTool(basicResult.getter));
|
|
989
|
+
// Register all setters
|
|
990
|
+
for (const setter of basicResult.setters) {
|
|
991
|
+
allTools.push(this.addTool(setter));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
// Basic mode: simple get/set tools
|
|
996
|
+
const basicResult = generateBasicStateTools({
|
|
997
|
+
name,
|
|
998
|
+
description,
|
|
999
|
+
get,
|
|
1000
|
+
set,
|
|
1001
|
+
schema: schema,
|
|
1002
|
+
});
|
|
1003
|
+
// Register getter
|
|
1004
|
+
allTools.push(this.addTool(basicResult.getter));
|
|
1005
|
+
// Register setter (should be single)
|
|
1006
|
+
for (const setter of basicResult.setters) {
|
|
1007
|
+
allTools.push(this.addTool(setter));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
// Build cleanup function
|
|
1011
|
+
const cleanup = () => {
|
|
1012
|
+
for (const tool of allTools) {
|
|
1013
|
+
this.removeTool(tool.name);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
// Return tuple: [getter, setter(s), cleanup]
|
|
1017
|
+
const getter = allTools[0];
|
|
1018
|
+
const settersOrExpanded = allTools.slice(1);
|
|
1019
|
+
// Return single setter if basic mode (no schemaSplit, no expand)
|
|
1020
|
+
if (!expand && !shouldDecompose) {
|
|
1021
|
+
return [getter, settersOrExpanded[0], cleanup];
|
|
1022
|
+
}
|
|
1023
|
+
return [getter, settersOrExpanded, cleanup];
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Whether the client is currently connected to the bridge server.
|
|
1027
|
+
*
|
|
1028
|
+
* @returns `true` if connected, `false` otherwise
|
|
1029
|
+
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```typescript
|
|
1032
|
+
* if (mcp.connected) {
|
|
1033
|
+
* console.log('Ready to receive tool calls');
|
|
1034
|
+
* } else {
|
|
1035
|
+
* await mcp.connect();
|
|
1036
|
+
* }
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
get connected() {
|
|
1040
|
+
return this.#connected;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Disconnects from the bridge server.
|
|
1044
|
+
*
|
|
1045
|
+
* Closes the WebSocket connection and cleans up event handlers.
|
|
1046
|
+
* Useful for cleanup when unmounting components or closing the application.
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```typescript
|
|
1050
|
+
* // In a Vue component lifecycle hook
|
|
1051
|
+
* onUnmounted(() => {
|
|
1052
|
+
* mcp.disconnect();
|
|
1053
|
+
* });
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
disconnect() {
|
|
1057
|
+
if (this.#ws) {
|
|
1058
|
+
this.#ws.onmessage = null;
|
|
1059
|
+
this.#ws.onopen = null;
|
|
1060
|
+
this.#ws.onerror = null;
|
|
1061
|
+
this.#ws.onclose = null;
|
|
1062
|
+
this.#ws.close();
|
|
1063
|
+
this.#ws = null;
|
|
1064
|
+
}
|
|
1065
|
+
this.#connected = false;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Gets list of all registered tool names.
|
|
1069
|
+
*
|
|
1070
|
+
* @returns Array of tool names
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* const toolNames = mcp.getTools();
|
|
1075
|
+
* console.log('Available tools:', toolNames);
|
|
1076
|
+
* ```
|
|
1077
|
+
*/
|
|
1078
|
+
getTools() {
|
|
1079
|
+
return Array.from(this.tools.keys());
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Triggers an AI agent query from your frontend code.
|
|
1083
|
+
*
|
|
1084
|
+
* Requires `agentUrl` to be configured in MCPWeb config. Sends a query to the AI agent
|
|
1085
|
+
* and returns a QueryResponse object that can be iterated to stream events.
|
|
1086
|
+
*
|
|
1087
|
+
* @param request - Query request with prompt and optional context
|
|
1088
|
+
* @param signal - Optional AbortSignal for canceling the query
|
|
1089
|
+
* @returns QueryResponse object that streams events
|
|
1090
|
+
* @throws {Error} If `agentUrl` is not configured or not connected to bridge
|
|
1091
|
+
*
|
|
1092
|
+
* @example Basic Query
|
|
1093
|
+
* ```typescript
|
|
1094
|
+
* const query = mcp.query({
|
|
1095
|
+
* prompt: 'Analyze the current todos and suggest priorities',
|
|
1096
|
+
* });
|
|
1097
|
+
*
|
|
1098
|
+
* for await (const event of query) {
|
|
1099
|
+
* if (event.type === 'query_complete') {
|
|
1100
|
+
* console.log('Result:', event.result);
|
|
1101
|
+
* }
|
|
1102
|
+
* }
|
|
1103
|
+
* ```
|
|
1104
|
+
*
|
|
1105
|
+
* @example With Context
|
|
1106
|
+
* ```typescript
|
|
1107
|
+
* const query = mcp.query({
|
|
1108
|
+
* prompt: 'Update the todo with highest priority',
|
|
1109
|
+
* context: [todosTool], // Provide specific tools as context
|
|
1110
|
+
* });
|
|
1111
|
+
*
|
|
1112
|
+
* for await (const event of query) {
|
|
1113
|
+
* console.log('Event:', event);
|
|
1114
|
+
* }
|
|
1115
|
+
* ```
|
|
1116
|
+
*
|
|
1117
|
+
* @example With Cancellation
|
|
1118
|
+
* ```typescript
|
|
1119
|
+
* const abortController = new AbortController();
|
|
1120
|
+
* const query = mcp.query(
|
|
1121
|
+
* { prompt: 'Long running task' },
|
|
1122
|
+
* abortController.signal
|
|
1123
|
+
* );
|
|
1124
|
+
*
|
|
1125
|
+
* // Cancel after 5 seconds
|
|
1126
|
+
* setTimeout(() => abortController.abort(), 5000);
|
|
1127
|
+
* ```
|
|
1128
|
+
*/
|
|
1129
|
+
query(request, signal) {
|
|
1130
|
+
if (!this.#config.agentUrl) {
|
|
1131
|
+
throw new Error('Agent URL not configured. Add agentUrl to MCPWeb config to enable queries.');
|
|
1132
|
+
}
|
|
1133
|
+
if (!this.#connected) {
|
|
1134
|
+
throw new Error('Not connected to bridge. Ensure MCPWeb is connected before making queries.');
|
|
1135
|
+
}
|
|
1136
|
+
const parsedRequest = QueryRequestSchema.parse(request);
|
|
1137
|
+
const { prompt, context, responseTool, timeout, } = parsedRequest;
|
|
1138
|
+
// Validate that responseTool is registered if provided
|
|
1139
|
+
if (responseTool && !this.tools.has(responseTool.name)) {
|
|
1140
|
+
throw new Error(`Response tool '${responseTool.name}' is not registered. Register it with addTool() first.`);
|
|
1141
|
+
}
|
|
1142
|
+
// Generate a unique identifier for the query
|
|
1143
|
+
const uuid = globalThis.crypto.randomUUID();
|
|
1144
|
+
// Create internal AbortController if none provided
|
|
1145
|
+
const internalAbortController = signal ? undefined : new AbortController();
|
|
1146
|
+
const effectiveSignal = signal || internalAbortController?.signal;
|
|
1147
|
+
// Create the async generator that will be wrapped in Query instance
|
|
1148
|
+
const stream = this.createQueryStream(uuid, prompt, context, responseTool, timeout, effectiveSignal);
|
|
1149
|
+
// Create cancel function that works with both external and internal AbortController
|
|
1150
|
+
const cancelFn = () => {
|
|
1151
|
+
if (internalAbortController) {
|
|
1152
|
+
internalAbortController.abort();
|
|
1153
|
+
}
|
|
1154
|
+
else if (this.#ws && this.#ws.readyState === WebSocket.OPEN) {
|
|
1155
|
+
// If using external signal, send cancel directly to bridge
|
|
1156
|
+
this.#ws.send(JSON.stringify({
|
|
1157
|
+
type: 'query_cancel',
|
|
1158
|
+
uuid
|
|
1159
|
+
}));
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
// Return QueryResponse instance with synchronous uuid access, async stream, and cancel function
|
|
1163
|
+
return new QueryResponse(uuid, stream, cancelFn);
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Internal method to create the query stream
|
|
1167
|
+
*/
|
|
1168
|
+
async *createQueryStream(uuid, prompt, context, responseTool, timeout, signal) {
|
|
1169
|
+
// Process context items
|
|
1170
|
+
const processedContext = [];
|
|
1171
|
+
if (context) {
|
|
1172
|
+
for (const item of context) {
|
|
1173
|
+
if ('handler' in item) {
|
|
1174
|
+
// Tool definition
|
|
1175
|
+
processedContext.push({
|
|
1176
|
+
name: item.name,
|
|
1177
|
+
value: await item.handler(),
|
|
1178
|
+
schema: item.outputSchema ? toJSONSchema(item.outputSchema) : undefined,
|
|
1179
|
+
description: item.description,
|
|
1180
|
+
type: 'tool'
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
// Ephemeral context
|
|
1185
|
+
processedContext.push({
|
|
1186
|
+
name: item.name,
|
|
1187
|
+
value: item.value,
|
|
1188
|
+
schema: item.schema,
|
|
1189
|
+
description: item.description,
|
|
1190
|
+
type: 'ephemeral'
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Convert responseTool to serializable metadata
|
|
1196
|
+
const responseToolMetadata = responseTool ? toToolMetadataJson(responseTool) : undefined;
|
|
1197
|
+
// Send query message to bridge
|
|
1198
|
+
const queryMessage = QueryMessageSchema.parse({
|
|
1199
|
+
uuid,
|
|
1200
|
+
prompt,
|
|
1201
|
+
context: processedContext,
|
|
1202
|
+
responseTool: responseToolMetadata
|
|
1203
|
+
});
|
|
1204
|
+
this.#ws?.send(JSON.stringify(queryMessage));
|
|
1205
|
+
// Create async iterator for streaming events
|
|
1206
|
+
let completed = false;
|
|
1207
|
+
let timeoutId = null;
|
|
1208
|
+
let aborted = false;
|
|
1209
|
+
const eventQueue = [];
|
|
1210
|
+
let resolveNext = null;
|
|
1211
|
+
// Handle abort signal
|
|
1212
|
+
const handleAbort = () => {
|
|
1213
|
+
if (aborted || completed)
|
|
1214
|
+
return;
|
|
1215
|
+
aborted = true;
|
|
1216
|
+
completed = true;
|
|
1217
|
+
// Send cancellation message to bridge
|
|
1218
|
+
if (this.#ws && this.#ws.readyState === WebSocket.OPEN) {
|
|
1219
|
+
this.#ws.send(JSON.stringify({
|
|
1220
|
+
type: 'query_cancel',
|
|
1221
|
+
uuid
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
if (timeoutId)
|
|
1225
|
+
clearTimeout(timeoutId);
|
|
1226
|
+
this.#ws?.removeEventListener('message', handleMessage);
|
|
1227
|
+
// Yield a cancellation response
|
|
1228
|
+
const cancelResponse = {
|
|
1229
|
+
type: 'query_cancel',
|
|
1230
|
+
uuid,
|
|
1231
|
+
};
|
|
1232
|
+
if (resolveNext) {
|
|
1233
|
+
resolveNext({ value: cancelResponse, done: false });
|
|
1234
|
+
resolveNext = null;
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
eventQueue.push(cancelResponse);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
// Listen for abort signal
|
|
1241
|
+
if (signal) {
|
|
1242
|
+
if (signal.aborted) {
|
|
1243
|
+
handleAbort();
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
signal.addEventListener('abort', handleAbort);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const handleMessage = (event) => {
|
|
1250
|
+
try {
|
|
1251
|
+
const message = JSON.parse(event.data);
|
|
1252
|
+
const parsedMessage = QueryResponseResultSchema.parse(message);
|
|
1253
|
+
if (parsedMessage.uuid === uuid) {
|
|
1254
|
+
if (parsedMessage.type === 'query_complete' || parsedMessage.type === 'query_failure') {
|
|
1255
|
+
completed = true;
|
|
1256
|
+
if (timeoutId)
|
|
1257
|
+
clearTimeout(timeoutId);
|
|
1258
|
+
this.#ws?.removeEventListener('message', handleMessage);
|
|
1259
|
+
if (signal)
|
|
1260
|
+
signal.removeEventListener('abort', handleAbort);
|
|
1261
|
+
}
|
|
1262
|
+
if (resolveNext) {
|
|
1263
|
+
resolveNext({ value: parsedMessage, done: false });
|
|
1264
|
+
resolveNext = null;
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
eventQueue.push(parsedMessage);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
catch (_error) {
|
|
1272
|
+
// Ignore invalid JSON
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
this.#ws?.addEventListener('message', handleMessage);
|
|
1276
|
+
// Set up timeout
|
|
1277
|
+
timeoutId = setTimeout(() => {
|
|
1278
|
+
completed = true;
|
|
1279
|
+
this.#ws?.removeEventListener('message', handleMessage);
|
|
1280
|
+
if (signal)
|
|
1281
|
+
signal.removeEventListener('abort', handleAbort);
|
|
1282
|
+
if (resolveNext) {
|
|
1283
|
+
resolveNext({ value: new Error('Query timeout'), done: true });
|
|
1284
|
+
resolveNext = null;
|
|
1285
|
+
}
|
|
1286
|
+
}, timeout);
|
|
1287
|
+
// Async iterator implementation
|
|
1288
|
+
const getNext = () => {
|
|
1289
|
+
return new Promise((resolve) => {
|
|
1290
|
+
if (eventQueue.length > 0) {
|
|
1291
|
+
// biome-ignore lint/style/noNonNullAssertion: We just checked that the length is greater than 0
|
|
1292
|
+
const event = eventQueue.shift();
|
|
1293
|
+
resolve({ value: event, done: false });
|
|
1294
|
+
}
|
|
1295
|
+
else if (completed) {
|
|
1296
|
+
resolve({ value: undefined, done: true });
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
resolveNext = resolve;
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
};
|
|
1303
|
+
// Yield events as they arrive
|
|
1304
|
+
while (!completed) {
|
|
1305
|
+
const result = await getNext();
|
|
1306
|
+
if (result.done)
|
|
1307
|
+
break;
|
|
1308
|
+
yield result.value;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
export default MCPWeb;
|