@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/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
- }