@mcp-web/client 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -3
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/standalone.js +18 -7
- package/package.json +11 -3
- package/esbuild.standalone.mjs +0 -17
- package/src/client.test.ts +0 -259
- package/src/client.ts +0 -783
- package/src/index.ts +0 -44
- package/src/schemas.ts +0 -38
- package/src/types.ts +0 -14
- package/src/utils.ts +0 -5
- package/tsconfig.json +0 -20
package/src/client.ts
DELETED
|
@@ -1,783 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ClientNotConextualizedErrorCode,
|
|
5
|
-
type ErroredListPromptsResult,
|
|
6
|
-
type ErroredListResourcesResult,
|
|
7
|
-
type ErroredListToolsResult,
|
|
8
|
-
type FatalError,
|
|
9
|
-
InvalidAuthenticationErrorCode,
|
|
10
|
-
type McpRequestMetaParams,
|
|
11
|
-
MissingAuthenticationErrorCode,
|
|
12
|
-
type Query,
|
|
13
|
-
QueryDoneErrorCode,
|
|
14
|
-
QueryNotActiveErrorCode,
|
|
15
|
-
QueryNotFoundErrorCode,
|
|
16
|
-
QuerySchema,
|
|
17
|
-
} from '@mcp-web/types';
|
|
18
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
-
import {
|
|
21
|
-
type CallToolRequest,
|
|
22
|
-
CallToolRequestSchema,
|
|
23
|
-
type CallToolResult,
|
|
24
|
-
ListPromptsRequestSchema,
|
|
25
|
-
type ListPromptsResult,
|
|
26
|
-
ListResourcesRequestSchema,
|
|
27
|
-
type ListResourcesResult,
|
|
28
|
-
ListToolsRequestSchema,
|
|
29
|
-
type ListToolsResult,
|
|
30
|
-
type ReadResourceRequest,
|
|
31
|
-
ReadResourceRequestSchema,
|
|
32
|
-
type ReadResourceResult,
|
|
33
|
-
type Tool,
|
|
34
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
35
|
-
import {
|
|
36
|
-
JsonRpcRequestSchema,
|
|
37
|
-
JsonRpcResponseSchema,
|
|
38
|
-
MCPWebClientConfigSchema,
|
|
39
|
-
} from './schemas.js';
|
|
40
|
-
import type {
|
|
41
|
-
Content,
|
|
42
|
-
MCPWebClientConfig,
|
|
43
|
-
MCPWebClientConfigOutput,
|
|
44
|
-
} from './types.js';
|
|
45
|
-
|
|
46
|
-
function isFatalError<T extends object>(result: T | FatalError): result is FatalError {
|
|
47
|
-
return 'errorIsFatal' in result && result.errorIsFatal === true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* MCP client that connects AI agents (like Claude Desktop) to the bridge server.
|
|
52
|
-
*
|
|
53
|
-
* MCPWebClient implements the MCP protocol and can run as a stdio server for
|
|
54
|
-
* AI host applications, or be used programmatically in agent server code.
|
|
55
|
-
*
|
|
56
|
-
* @example Running as MCP server for Claude Desktop
|
|
57
|
-
* ```typescript
|
|
58
|
-
* const client = new MCPWebClient({
|
|
59
|
-
* serverUrl: 'http://localhost:3001',
|
|
60
|
-
* authToken: 'your-auth-token',
|
|
61
|
-
* });
|
|
62
|
-
* await client.run(); // Starts stdio transport
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @example Programmatic usage in agent code
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const client = new MCPWebClient({
|
|
68
|
-
* serverUrl: 'http://localhost:3001',
|
|
69
|
-
* authToken: 'your-auth-token',
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* // List available tools
|
|
73
|
-
* const { tools } = await client.listTools();
|
|
74
|
-
*
|
|
75
|
-
* // Call a tool
|
|
76
|
-
* const result = await client.callTool('get_todos');
|
|
77
|
-
* ```
|
|
78
|
-
*
|
|
79
|
-
* @example With query context (for agent servers)
|
|
80
|
-
* ```typescript
|
|
81
|
-
* const contextualClient = client.contextualize(query);
|
|
82
|
-
* const result = await contextualClient.callTool('update_todo', { id: '1' });
|
|
83
|
-
* await contextualClient.complete('Todo updated successfully');
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export class MCPWebClient {
|
|
87
|
-
#config: MCPWebClientConfigOutput;
|
|
88
|
-
#server?: Server;
|
|
89
|
-
#query?: Query;
|
|
90
|
-
#isDone = false; // Track if query has been completed
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Creates a new MCPWebClient instance.
|
|
94
|
-
*
|
|
95
|
-
* @param config - Client configuration with server URL and auth token
|
|
96
|
-
* @param query - Optional query for contextualized instances (internal use)
|
|
97
|
-
*/
|
|
98
|
-
constructor(config: MCPWebClientConfig, query?: Query) {
|
|
99
|
-
this.#config = MCPWebClientConfigSchema.parse(config);
|
|
100
|
-
|
|
101
|
-
if (query) {
|
|
102
|
-
this.#query = QuerySchema.parse(query);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private getMetaParams(sessionId?: string): McpRequestMetaParams | undefined {
|
|
107
|
-
if (sessionId || this.#query?.uuid) {
|
|
108
|
-
const meta: McpRequestMetaParams = {};
|
|
109
|
-
if (sessionId) {
|
|
110
|
-
meta.sessionId = sessionId;
|
|
111
|
-
}
|
|
112
|
-
if (this.#query?.uuid) {
|
|
113
|
-
meta.queryId = this.#query.uuid;
|
|
114
|
-
}
|
|
115
|
-
return meta;
|
|
116
|
-
}
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private getParams(sessionId?: string): { _meta: McpRequestMetaParams } | undefined {
|
|
121
|
-
const meta = this.getMetaParams(sessionId);
|
|
122
|
-
if (meta) {
|
|
123
|
-
return { _meta: meta };
|
|
124
|
-
}
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async makeToolCallRequest(request: CallToolRequest): Promise<CallToolResult> {
|
|
129
|
-
try {
|
|
130
|
-
const { name, arguments: args, _meta } = request.params as any;
|
|
131
|
-
|
|
132
|
-
const response = await this.makeRequest('tools/call', {
|
|
133
|
-
name,
|
|
134
|
-
arguments: args || {},
|
|
135
|
-
...(_meta && { _meta })
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Check if response is already in CallToolResult format (from bridge)
|
|
139
|
-
// This happens when the bridge wraps results for Remote MCP compatibility
|
|
140
|
-
if (
|
|
141
|
-
response &&
|
|
142
|
-
typeof response === 'object' &&
|
|
143
|
-
'content' in response &&
|
|
144
|
-
Array.isArray((response as { content: unknown }).content)
|
|
145
|
-
) {
|
|
146
|
-
return response as CallToolResult;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Handle different response formats (legacy/unwrapped responses)
|
|
150
|
-
// Check if this is an error response from bridge
|
|
151
|
-
if (response && typeof response === 'object' && 'error' in response) {
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
155
|
-
type: 'text',
|
|
156
|
-
text: JSON.stringify(response, null, 2),
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
isError: true
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Handle successful responses
|
|
164
|
-
// The response could be:
|
|
165
|
-
// 1. A wrapped response: { data: <actual data> }
|
|
166
|
-
// 2. Direct tool result: any type (string, number, object, etc.)
|
|
167
|
-
let content: Content[];
|
|
168
|
-
let topLevelMeta: Record<string, unknown> | undefined;
|
|
169
|
-
let actualData = (response && typeof response === 'object' && 'data' in response)
|
|
170
|
-
? response.data
|
|
171
|
-
: response;
|
|
172
|
-
|
|
173
|
-
// Extract _meta from the data to place at the top level of CallToolResult.
|
|
174
|
-
// The MCP protocol expects _meta as a top-level field on the result object,
|
|
175
|
-
// not embedded inside the JSON text content.
|
|
176
|
-
if (actualData && typeof actualData === 'object' && '_meta' in actualData) {
|
|
177
|
-
const { _meta: extractedMeta, ...rest } = actualData as Record<string, unknown>;
|
|
178
|
-
if (extractedMeta && typeof extractedMeta === 'object') {
|
|
179
|
-
topLevelMeta = extractedMeta as Record<string, unknown>;
|
|
180
|
-
}
|
|
181
|
-
actualData = rest;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (typeof actualData === 'string') {
|
|
185
|
-
// Check if it's a data URL (image)
|
|
186
|
-
if (actualData.startsWith('data:image/')) {
|
|
187
|
-
content = [
|
|
188
|
-
{
|
|
189
|
-
type: 'image',
|
|
190
|
-
data: actualData.split(',')[1],
|
|
191
|
-
mimeType: actualData.split(';')[0].split(':')[1],
|
|
192
|
-
},
|
|
193
|
-
];
|
|
194
|
-
} else {
|
|
195
|
-
content = [
|
|
196
|
-
{
|
|
197
|
-
type: 'text',
|
|
198
|
-
text: actualData
|
|
199
|
-
}
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
} else if (actualData !== null && actualData !== undefined) {
|
|
203
|
-
content = [
|
|
204
|
-
{
|
|
205
|
-
type: 'text',
|
|
206
|
-
text: typeof actualData === 'object' ? JSON.stringify(actualData, null, 2) : String(actualData)
|
|
207
|
-
}
|
|
208
|
-
];
|
|
209
|
-
} else {
|
|
210
|
-
// null or undefined result
|
|
211
|
-
content = [
|
|
212
|
-
{
|
|
213
|
-
type: 'text',
|
|
214
|
-
text: ''
|
|
215
|
-
}
|
|
216
|
-
];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const callToolResult: CallToolResult = { content };
|
|
220
|
-
if (topLevelMeta) {
|
|
221
|
-
callToolResult._meta = topLevelMeta;
|
|
222
|
-
}
|
|
223
|
-
return callToolResult;
|
|
224
|
-
|
|
225
|
-
} catch (error) {
|
|
226
|
-
// Re-throw authentication and query errors
|
|
227
|
-
if (error instanceof Error) {
|
|
228
|
-
const errorMessage = error.message;
|
|
229
|
-
if (errorMessage === MissingAuthenticationErrorCode ||
|
|
230
|
-
errorMessage === InvalidAuthenticationErrorCode ||
|
|
231
|
-
errorMessage === QueryNotFoundErrorCode ||
|
|
232
|
-
errorMessage === QueryNotActiveErrorCode) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// All other errors get returned as CallToolResult with isError: true
|
|
238
|
-
return {
|
|
239
|
-
content: [
|
|
240
|
-
{
|
|
241
|
-
type: 'text',
|
|
242
|
-
text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
243
|
-
}
|
|
244
|
-
],
|
|
245
|
-
isError: true
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private async makeListToolsRequest(sessionId?: string): Promise<ListToolsResult | ErroredListToolsResult> {
|
|
251
|
-
const response = await this.makeRequest<ListToolsResult | ErroredListToolsResult | FatalError>('tools/list', this.getParams(sessionId));
|
|
252
|
-
|
|
253
|
-
if (isFatalError(response)) {
|
|
254
|
-
throw new Error(response.error_message);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return response;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private async makeListResourcesRequest(sessionId?: string): Promise<ListResourcesResult | ErroredListResourcesResult> {
|
|
261
|
-
const response = await this.makeRequest<ListResourcesResult | ErroredListResourcesResult | FatalError>('resources/list', this.getParams(sessionId));
|
|
262
|
-
|
|
263
|
-
if (isFatalError(response)) {
|
|
264
|
-
throw new Error(response.error_message);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return response;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private async makeListPromptsRequest(sessionId?: string): Promise<ListPromptsResult | ErroredListPromptsResult> {
|
|
271
|
-
const response = await this.makeRequest<ListPromptsResult | ErroredListPromptsResult | FatalError>('prompts/list', this.getParams(sessionId));
|
|
272
|
-
|
|
273
|
-
if (isFatalError(response)) {
|
|
274
|
-
throw new Error(response.error_message);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return response;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async makeReadResourceRequest(request: ReadResourceRequest): Promise<ReadResourceResult> {
|
|
281
|
-
const { uri, _meta } = request.params;
|
|
282
|
-
|
|
283
|
-
const response = await this.makeRequest<ReadResourceResult | FatalError>('resources/read', {
|
|
284
|
-
uri,
|
|
285
|
-
...(_meta && { _meta }),
|
|
286
|
-
...this.getParams(),
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
if (isFatalError(response)) {
|
|
290
|
-
throw new Error(response.error_message);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return response;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private setupHandlers() {
|
|
297
|
-
if (!this.#server) return;
|
|
298
|
-
|
|
299
|
-
// Handle tool listing
|
|
300
|
-
this.#server.setRequestHandler(ListToolsRequestSchema, () => this.makeListToolsRequest());
|
|
301
|
-
|
|
302
|
-
// Handle tool calls
|
|
303
|
-
this.#server.setRequestHandler(CallToolRequestSchema, this.makeToolCallRequest.bind(this));
|
|
304
|
-
|
|
305
|
-
// Handle resource listing
|
|
306
|
-
this.#server.setRequestHandler(ListResourcesRequestSchema, () => this.makeListResourcesRequest());
|
|
307
|
-
|
|
308
|
-
// Handle resource reading
|
|
309
|
-
this.#server.setRequestHandler(ReadResourceRequestSchema, (request: ReadResourceRequest) => this.makeReadResourceRequest(request));
|
|
310
|
-
|
|
311
|
-
// Handle prompt listing
|
|
312
|
-
this.#server.setRequestHandler(ListPromptsRequestSchema, () => this.makeListPromptsRequest());
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Creates a contextualized client for a specific query.
|
|
317
|
-
*
|
|
318
|
-
* All tool calls made through the returned client will be tagged with the
|
|
319
|
-
* query UUID, enabling the bridge to track tool calls for that query.
|
|
320
|
-
*
|
|
321
|
-
* @param query - The query object containing uuid and optional responseTool
|
|
322
|
-
* @returns A new MCPWebClient instance bound to the query context
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
* ```typescript
|
|
326
|
-
* const contextualClient = client.contextualize(query);
|
|
327
|
-
* await contextualClient.callTool('analyze_data');
|
|
328
|
-
* await contextualClient.complete('Analysis complete');
|
|
329
|
-
* ```
|
|
330
|
-
*/
|
|
331
|
-
contextualize(query: Query): MCPWebClient {
|
|
332
|
-
return new MCPWebClient(this.#config, query);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Calls a tool on the connected frontend.
|
|
337
|
-
*
|
|
338
|
-
* Automatically includes query context if this is a contextualized client.
|
|
339
|
-
* If the query has tool restrictions, only allowed tools can be called.
|
|
340
|
-
*
|
|
341
|
-
* @param name - Name of the tool to call
|
|
342
|
-
* @param args - Optional arguments to pass to the tool
|
|
343
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
344
|
-
* @returns Tool execution result
|
|
345
|
-
* @throws {Error} If query is already done or tool is not allowed
|
|
346
|
-
*
|
|
347
|
-
* @example
|
|
348
|
-
* ```typescript
|
|
349
|
-
* const result = await client.callTool('create_todo', {
|
|
350
|
-
* title: 'New task',
|
|
351
|
-
* priority: 'high',
|
|
352
|
-
* });
|
|
353
|
-
* ```
|
|
354
|
-
*/
|
|
355
|
-
async callTool(name: string, args?: Record<string, unknown>, sessionId?: string): Promise<CallToolResult> {
|
|
356
|
-
if (this.#query && this.#isDone) {
|
|
357
|
-
throw new Error(QueryDoneErrorCode);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check tool restrictions if query has them
|
|
361
|
-
if (this.#query?.restrictTools && this.#query?.tools) {
|
|
362
|
-
const allowed = this.#query.tools.some(t => t.name === name);
|
|
363
|
-
if (!allowed) {
|
|
364
|
-
throw new Error(
|
|
365
|
-
`Tool '${name}' not allowed. Query restricted to: ${this.#query.tools.map(t => t.name).join(', ')}`
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const request: CallToolRequest = {
|
|
371
|
-
method: 'tools/call',
|
|
372
|
-
params: {
|
|
373
|
-
name,
|
|
374
|
-
arguments: args || {} as Record<string, unknown>,
|
|
375
|
-
// Augment with query context if this is a contextualized instance
|
|
376
|
-
...this.getParams(sessionId)
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const response = await this.makeToolCallRequest(request);
|
|
381
|
-
|
|
382
|
-
// Auto-complete if this was the responseTool and it succeeded
|
|
383
|
-
// Note: response.isError is true for errors, undefined for success
|
|
384
|
-
if (this.#query?.responseTool?.name === name && response.isError !== true) {
|
|
385
|
-
this.#isDone = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return response;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Lists all available tools from the connected frontend.
|
|
393
|
-
*
|
|
394
|
-
* If this is a contextualized client with restricted tools, returns only
|
|
395
|
-
* those tools. Otherwise fetches all tools from the bridge.
|
|
396
|
-
*
|
|
397
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
398
|
-
* @returns List of available tools
|
|
399
|
-
* @throws {Error} If query is already done
|
|
400
|
-
*/
|
|
401
|
-
async listTools(sessionId?: string): Promise<ListToolsResult | ErroredListToolsResult> {
|
|
402
|
-
if (this.#isDone) {
|
|
403
|
-
throw new Error(QueryDoneErrorCode);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// If we have tools from the query, return those
|
|
407
|
-
if (this.#query?.tools) {
|
|
408
|
-
// Need to convert ToolDefinition to Tool format expected by MCP
|
|
409
|
-
const tools = this.#query.tools.map(t => ({
|
|
410
|
-
name: t.name,
|
|
411
|
-
description: t.description,
|
|
412
|
-
inputSchema: t.inputSchema || { type: 'object', properties: {}, required: [] }
|
|
413
|
-
}));
|
|
414
|
-
return { tools: tools as Tool[] };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Otherwise use the shared request handler
|
|
418
|
-
return this.makeListToolsRequest(sessionId);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Lists all available resources from the connected frontend.
|
|
423
|
-
*
|
|
424
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
425
|
-
* @returns List of available resources
|
|
426
|
-
* @throws {Error} If query is already done
|
|
427
|
-
*/
|
|
428
|
-
async listResources(sessionId?: string): Promise<ListResourcesResult | ErroredListResourcesResult> {
|
|
429
|
-
if (this.#isDone) {
|
|
430
|
-
throw new Error(QueryDoneErrorCode);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return this.makeListResourcesRequest(sessionId);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Lists all available prompts from the connected frontend.
|
|
438
|
-
*
|
|
439
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
440
|
-
* @returns List of available prompts
|
|
441
|
-
* @throws {Error} If query is already done
|
|
442
|
-
*/
|
|
443
|
-
async listPrompts(sessionId?: string): Promise<ListPromptsResult | ErroredListPromptsResult> {
|
|
444
|
-
if (this.#isDone) {
|
|
445
|
-
throw new Error(QueryDoneErrorCode);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return this.makeListPromptsRequest(sessionId);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Sends a progress update for the current query.
|
|
453
|
-
*
|
|
454
|
-
* Use this to provide intermediate updates during long-running operations.
|
|
455
|
-
* Can only be called on a contextualized client instance.
|
|
456
|
-
*
|
|
457
|
-
* @param message - Progress message to send to the frontend
|
|
458
|
-
* @throws {Error} If not a contextualized client or query is done
|
|
459
|
-
*
|
|
460
|
-
* @example
|
|
461
|
-
* ```typescript
|
|
462
|
-
* await contextualClient.sendProgress('Processing step 1 of 3...');
|
|
463
|
-
* // ... do work ...
|
|
464
|
-
* await contextualClient.sendProgress('Processing step 2 of 3...');
|
|
465
|
-
* ```
|
|
466
|
-
*/
|
|
467
|
-
async sendProgress(message: string): Promise<void> {
|
|
468
|
-
if (!this.#query) {
|
|
469
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (this.#isDone) {
|
|
473
|
-
throw new Error(QueryDoneErrorCode);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
477
|
-
const progressUrl = `${url}/query/${this.#query.uuid}/progress`;
|
|
478
|
-
const response = await fetch(progressUrl, {
|
|
479
|
-
method: 'POST',
|
|
480
|
-
headers: {
|
|
481
|
-
'Content-Type': 'application/json',
|
|
482
|
-
},
|
|
483
|
-
body: JSON.stringify({ message })
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
488
|
-
throw new Error(errorData.error || `Failed to send progress: HTTP ${response.status}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Marks the current query as complete with a message.
|
|
494
|
-
*
|
|
495
|
-
* Can only be called on a contextualized client instance.
|
|
496
|
-
* If the query specified a responseTool, call that tool instead - calling
|
|
497
|
-
* this method will result in an error.
|
|
498
|
-
*
|
|
499
|
-
* @param message - Completion message to send to the frontend
|
|
500
|
-
* @throws {Error} If not a contextualized client, query is done, or responseTool was specified
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```typescript
|
|
504
|
-
* await contextualClient.complete('Analysis complete: found 5 issues');
|
|
505
|
-
* ```
|
|
506
|
-
*/
|
|
507
|
-
async complete(message: string): Promise<void> {
|
|
508
|
-
if (!this.#query) {
|
|
509
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (this.#isDone) {
|
|
513
|
-
throw new Error(QueryDoneErrorCode);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
517
|
-
const completeUrl = `${url}/query/${this.#query.uuid}/complete`;
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
const response = await fetch(completeUrl, {
|
|
521
|
-
method: 'PUT',
|
|
522
|
-
headers: {
|
|
523
|
-
'Content-Type': 'application/json',
|
|
524
|
-
},
|
|
525
|
-
body: JSON.stringify({ message })
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
if (!response.ok) {
|
|
529
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
530
|
-
throw new Error(`Failed to complete query: ${errorData.error || response.statusText}`);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Only mark as completed after successful response
|
|
534
|
-
this.#isDone = true;
|
|
535
|
-
} catch (error) {
|
|
536
|
-
throw error;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Marks the current query as failed with an error message.
|
|
542
|
-
*
|
|
543
|
-
* Can only be called on a contextualized client instance.
|
|
544
|
-
* Use this when the query encounters an unrecoverable error.
|
|
545
|
-
*
|
|
546
|
-
* @param error - Error message or Error object describing the failure
|
|
547
|
-
* @throws {Error} If not a contextualized client or query is already done
|
|
548
|
-
*
|
|
549
|
-
* @example
|
|
550
|
-
* ```typescript
|
|
551
|
-
* try {
|
|
552
|
-
* await contextualClient.callTool('risky_operation');
|
|
553
|
-
* } catch (e) {
|
|
554
|
-
* await contextualClient.fail(e);
|
|
555
|
-
* }
|
|
556
|
-
* ```
|
|
557
|
-
*/
|
|
558
|
-
async fail(error: string | Error): Promise<void> {
|
|
559
|
-
if (!this.#query) {
|
|
560
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (this.#isDone) {
|
|
564
|
-
throw new Error(QueryDoneErrorCode);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const errorMessage = typeof error === 'string' ? error : error.message;
|
|
568
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
569
|
-
const failUrl = `${url}/query/${this.#query.uuid}/fail`;
|
|
570
|
-
|
|
571
|
-
try {
|
|
572
|
-
const response = await fetch(failUrl, {
|
|
573
|
-
method: 'PUT',
|
|
574
|
-
headers: {
|
|
575
|
-
'Content-Type': 'application/json',
|
|
576
|
-
},
|
|
577
|
-
body: JSON.stringify({ error: errorMessage })
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
if (!response.ok) {
|
|
581
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
582
|
-
throw new Error(`Failed to mark query as failed: ${errorData.error || response.statusText}`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Mark as completed to prevent further operations
|
|
586
|
-
this.#isDone = true;
|
|
587
|
-
} catch (err) {
|
|
588
|
-
throw err;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Cancels the current query.
|
|
594
|
-
*
|
|
595
|
-
* Can only be called on a contextualized client instance.
|
|
596
|
-
* Use this when the user or system needs to abort query processing.
|
|
597
|
-
*
|
|
598
|
-
* @param reason - Optional reason for the cancellation
|
|
599
|
-
* @throws {Error} If not a contextualized client or query is already done
|
|
600
|
-
*
|
|
601
|
-
* @example
|
|
602
|
-
* ```typescript
|
|
603
|
-
* // User requested cancellation
|
|
604
|
-
* await contextualClient.cancel('User cancelled operation');
|
|
605
|
-
* ```
|
|
606
|
-
*/
|
|
607
|
-
async cancel(reason?: string): Promise<void> {
|
|
608
|
-
if (!this.#query) {
|
|
609
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (this.#isDone) {
|
|
613
|
-
throw new Error(QueryDoneErrorCode);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
617
|
-
const cancelUrl = `${url}/query/${this.#query.uuid}/cancel`;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
const response = await fetch(cancelUrl, {
|
|
621
|
-
method: 'PUT',
|
|
622
|
-
headers: {
|
|
623
|
-
'Content-Type': 'application/json',
|
|
624
|
-
},
|
|
625
|
-
body: JSON.stringify(reason ? { reason } : {})
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
if (!response.ok) {
|
|
629
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
630
|
-
throw new Error(`Failed to cancel query: ${errorData.error || response.statusText}`);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Mark as completed to prevent further operations
|
|
634
|
-
this.#isDone = true;
|
|
635
|
-
} catch (err) {
|
|
636
|
-
throw err;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private async makeRequest<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
641
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
642
|
-
|
|
643
|
-
const requestBody = JsonRpcRequestSchema.parse({
|
|
644
|
-
jsonrpc: '2.0',
|
|
645
|
-
id: Date.now(),
|
|
646
|
-
method,
|
|
647
|
-
params
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
const controller = new AbortController();
|
|
652
|
-
const timeoutId = setTimeout(() => controller.abort(), this.#config.timeout);
|
|
653
|
-
|
|
654
|
-
// Only include Authorization header if we have an authToken
|
|
655
|
-
const headers: Record<string, string> = {
|
|
656
|
-
'Content-Type': 'application/json',
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
if (this.#config.authToken) {
|
|
660
|
-
headers.Authorization = `Bearer ${this.#config.authToken}`;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const response = await fetch(url, {
|
|
664
|
-
method: 'POST',
|
|
665
|
-
headers,
|
|
666
|
-
body: JSON.stringify(requestBody),
|
|
667
|
-
signal: controller.signal
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
clearTimeout(timeoutId);
|
|
671
|
-
|
|
672
|
-
if (!response.ok) {
|
|
673
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const rawData = await response.json();
|
|
677
|
-
|
|
678
|
-
const data = JsonRpcResponseSchema.parse(rawData);
|
|
679
|
-
|
|
680
|
-
if (data.error) {
|
|
681
|
-
throw new Error(data.error.message);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return data.result as T;
|
|
685
|
-
|
|
686
|
-
} catch (error: unknown) {
|
|
687
|
-
if (error instanceof Error) {
|
|
688
|
-
if (error.name === 'AbortError') {
|
|
689
|
-
throw new Error('Request timeout');
|
|
690
|
-
}
|
|
691
|
-
throw error;
|
|
692
|
-
}
|
|
693
|
-
throw new Error(`Unknown error: ${error}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Fetches server identity (name, version, icon) from the bridge.
|
|
699
|
-
* Falls back to defaults if the bridge is unreachable.
|
|
700
|
-
*/
|
|
701
|
-
private async fetchBridgeInfo(): Promise<{
|
|
702
|
-
name: string;
|
|
703
|
-
version: string;
|
|
704
|
-
icon?: string;
|
|
705
|
-
}> {
|
|
706
|
-
const defaults = { name: '@mcp-web/client', version: '1.0.0' };
|
|
707
|
-
try {
|
|
708
|
-
const url = this.#config.serverUrl
|
|
709
|
-
.replace('ws:', 'http:')
|
|
710
|
-
.replace('wss:', 'https:');
|
|
711
|
-
|
|
712
|
-
const controller = new AbortController();
|
|
713
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
714
|
-
|
|
715
|
-
const response = await fetch(url, {
|
|
716
|
-
method: 'GET',
|
|
717
|
-
signal: controller.signal,
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
clearTimeout(timeoutId);
|
|
721
|
-
|
|
722
|
-
if (!response.ok) return defaults;
|
|
723
|
-
|
|
724
|
-
const data = (await response.json()) as Record<string, unknown>;
|
|
725
|
-
return {
|
|
726
|
-
name: typeof data.name === 'string' ? data.name : defaults.name,
|
|
727
|
-
version:
|
|
728
|
-
typeof data.version === 'string'
|
|
729
|
-
? data.version
|
|
730
|
-
: defaults.version,
|
|
731
|
-
...(typeof data.icon === 'string' && { icon: data.icon }),
|
|
732
|
-
};
|
|
733
|
-
} catch {
|
|
734
|
-
return defaults;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Starts the MCP server using stdio transport.
|
|
740
|
-
*
|
|
741
|
-
* This method is intended for running as a subprocess of an AI host like
|
|
742
|
-
* Claude Desktop. It connects to stdin/stdout for MCP communication.
|
|
743
|
-
*
|
|
744
|
-
* Cannot be called on contextualized client instances.
|
|
745
|
-
*
|
|
746
|
-
* @throws {Error} If called on a contextualized client or server not initialized
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* ```typescript
|
|
750
|
-
* // In your entry point script
|
|
751
|
-
* const client = new MCPWebClient(config);
|
|
752
|
-
* await client.run();
|
|
753
|
-
* ```
|
|
754
|
-
*/
|
|
755
|
-
async run() {
|
|
756
|
-
if (this.#query) {
|
|
757
|
-
throw new Error('Cannot run a contextualized client instance. Only root clients can be run as MCP servers.');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Fetch bridge identity before creating the MCP server
|
|
761
|
-
const bridgeInfo = await this.fetchBridgeInfo();
|
|
762
|
-
|
|
763
|
-
this.#server = new Server(
|
|
764
|
-
{
|
|
765
|
-
name: bridgeInfo.name,
|
|
766
|
-
version: bridgeInfo.version,
|
|
767
|
-
...(bridgeInfo.icon && { icon: bridgeInfo.icon }),
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
capabilities: {
|
|
771
|
-
tools: {},
|
|
772
|
-
resources: {},
|
|
773
|
-
prompts: {},
|
|
774
|
-
},
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
this.setupHandlers();
|
|
779
|
-
|
|
780
|
-
const transport = new StdioServerTransport();
|
|
781
|
-
await this.#server.connect(transport);
|
|
782
|
-
}
|
|
783
|
-
}
|