@mastra/mcp 0.11.3-alpha.0 → 0.11.3-alpha.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.cjs +7 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.js +7 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/server/server.d.ts.map +1 -1
  7. package/package.json +19 -6
  8. package/.turbo/turbo-build.log +0 -4
  9. package/eslint.config.js +0 -11
  10. package/integration-tests/node_modules/.bin/tsc +0 -21
  11. package/integration-tests/node_modules/.bin/tsserver +0 -21
  12. package/integration-tests/node_modules/.bin/vitest +0 -21
  13. package/integration-tests/package.json +0 -29
  14. package/integration-tests/src/mastra/agents/weather.ts +0 -34
  15. package/integration-tests/src/mastra/index.ts +0 -15
  16. package/integration-tests/src/mastra/mcp/index.ts +0 -46
  17. package/integration-tests/src/mastra/tools/weather.ts +0 -13
  18. package/integration-tests/src/server.test.ts +0 -238
  19. package/integration-tests/tsconfig.json +0 -13
  20. package/integration-tests/vitest.config.ts +0 -14
  21. package/src/__fixtures__/fire-crawl-complex-schema.ts +0 -1013
  22. package/src/__fixtures__/server-weather.ts +0 -16
  23. package/src/__fixtures__/stock-price.ts +0 -128
  24. package/src/__fixtures__/tools.ts +0 -94
  25. package/src/__fixtures__/weather.ts +0 -269
  26. package/src/client/client.test.ts +0 -585
  27. package/src/client/client.ts +0 -628
  28. package/src/client/configuration.test.ts +0 -856
  29. package/src/client/configuration.ts +0 -468
  30. package/src/client/elicitationActions.ts +0 -26
  31. package/src/client/index.ts +0 -3
  32. package/src/client/promptActions.ts +0 -70
  33. package/src/client/resourceActions.ts +0 -119
  34. package/src/index.ts +0 -2
  35. package/src/server/index.ts +0 -2
  36. package/src/server/promptActions.ts +0 -48
  37. package/src/server/resourceActions.ts +0 -90
  38. package/src/server/server-logging.test.ts +0 -181
  39. package/src/server/server.test.ts +0 -2142
  40. package/src/server/server.ts +0 -1442
  41. package/src/server/types.ts +0 -59
  42. package/tsconfig.build.json +0 -9
  43. package/tsconfig.json +0 -5
  44. package/tsup.config.ts +0 -17
  45. package/vitest.config.ts +0 -8
@@ -1,1442 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import type * as http from 'node:http';
3
- import type { ToolsInput, Agent } from '@mastra/core/agent';
4
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
5
- import { MCPServerBase } from '@mastra/core/mcp';
6
- import type {
7
- MCPServerConfig,
8
- ServerInfo,
9
- ServerDetailInfo,
10
- ConvertedTool,
11
- MCPServerHonoSSEOptions,
12
- MCPServerSSEOptions,
13
- MCPToolType,
14
- } from '@mastra/core/mcp';
15
- import { RuntimeContext } from '@mastra/core/runtime-context';
16
- import { createTool } from '@mastra/core/tools';
17
- import type { InternalCoreTool } from '@mastra/core/tools';
18
- import { makeCoreTool } from '@mastra/core/utils';
19
- import type { Workflow } from '@mastra/core/workflows';
20
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
21
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
22
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
24
- import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
25
- import {
26
- CallToolRequestSchema,
27
- ListToolsRequestSchema,
28
- ListResourcesRequestSchema,
29
- ReadResourceRequestSchema,
30
- ListResourceTemplatesRequestSchema,
31
- SubscribeRequestSchema,
32
- UnsubscribeRequestSchema,
33
- ListPromptsRequestSchema,
34
- GetPromptRequestSchema,
35
- PromptSchema,
36
- } from '@modelcontextprotocol/sdk/types.js';
37
- import type {
38
- ResourceContents,
39
- Resource,
40
- ResourceTemplate,
41
- ServerCapabilities,
42
- Prompt,
43
- CallToolResult,
44
- ElicitResult,
45
- ElicitRequest,
46
- } from '@modelcontextprotocol/sdk/types.js';
47
- import type { SSEStreamingApi } from 'hono/streaming';
48
- import { streamSSE } from 'hono/streaming';
49
- import { SSETransport } from 'hono-mcp-server-sse-transport';
50
- import { z } from 'zod';
51
- import { ServerPromptActions } from './promptActions';
52
- import { ServerResourceActions } from './resourceActions';
53
- import type { MCPServerPrompts, MCPServerResources, ElicitationActions, MCPTool } from './types';
54
- export class MCPServer extends MCPServerBase {
55
- private server: Server;
56
- private stdioTransport?: StdioServerTransport;
57
- private sseTransport?: SSEServerTransport;
58
- private sseHonoTransports: Map<string, SSETransport>;
59
- private streamableHTTPTransports: Map<string, StreamableHTTPServerTransport> = new Map();
60
- // Track server instances for each HTTP session
61
- private httpServerInstances: Map<string, Server> = new Map();
62
-
63
- private definedResources?: Resource[];
64
- private definedResourceTemplates?: ResourceTemplate[];
65
- private resourceOptions?: MCPServerResources;
66
- private definedPrompts?: Prompt[];
67
- private promptOptions?: MCPServerPrompts;
68
- private subscriptions: Set<string> = new Set();
69
- public readonly resources: ServerResourceActions;
70
- public readonly prompts: ServerPromptActions;
71
- public readonly elicitation: ElicitationActions;
72
-
73
- /**
74
- * Get the current stdio transport.
75
- */
76
- public getStdioTransport(): StdioServerTransport | undefined {
77
- return this.stdioTransport;
78
- }
79
-
80
- /**
81
- * Get the current SSE transport.
82
- */
83
- public getSseTransport(): SSEServerTransport | undefined {
84
- return this.sseTransport;
85
- }
86
-
87
- /**
88
- * Get the current SSE Hono transport.
89
- */
90
- public getSseHonoTransport(sessionId: string): SSETransport | undefined {
91
- return this.sseHonoTransports.get(sessionId);
92
- }
93
-
94
- /**
95
- * Get the current server instance.
96
- */
97
- public getServer(): Server {
98
- return this.server;
99
- }
100
-
101
- /**
102
- * Construct a new MCPServer instance.
103
- * @param opts - Configuration options for the server, including registry metadata.
104
- */
105
- constructor(opts: MCPServerConfig & { resources?: MCPServerResources; prompts?: MCPServerPrompts }) {
106
- super(opts);
107
- this.resourceOptions = opts.resources;
108
- this.promptOptions = opts.prompts;
109
-
110
- const capabilities: ServerCapabilities = {
111
- tools: {},
112
- logging: { enabled: true },
113
- elicitation: {},
114
- };
115
-
116
- if (opts.resources) {
117
- capabilities.resources = { subscribe: true, listChanged: true };
118
- }
119
-
120
- if (opts.prompts) {
121
- capabilities.prompts = { listChanged: true };
122
- }
123
-
124
- this.server = new Server({ name: this.name, version: this.version }, { capabilities });
125
-
126
- this.logger.info(
127
- `Initialized MCPServer '${this.name}' v${this.version} (ID: ${this.id}) with tools: ${Object.keys(this.convertedTools).join(', ')} and resources. Capabilities: ${JSON.stringify(capabilities)}`,
128
- );
129
-
130
- this.sseHonoTransports = new Map();
131
-
132
- // Register all handlers on the main server instance
133
- this.registerHandlersOnServer(this.server);
134
-
135
- this.resources = new ServerResourceActions({
136
- getSubscriptions: () => this.subscriptions,
137
- getLogger: () => this.logger,
138
- getSdkServer: () => this.server,
139
- clearDefinedResources: () => {
140
- this.definedResources = undefined;
141
- },
142
- clearDefinedResourceTemplates: () => {
143
- this.definedResourceTemplates = undefined;
144
- },
145
- });
146
-
147
- this.prompts = new ServerPromptActions({
148
- getLogger: () => this.logger,
149
- getSdkServer: () => this.server,
150
- clearDefinedPrompts: () => {
151
- this.definedPrompts = undefined;
152
- },
153
- });
154
-
155
- this.elicitation = {
156
- sendRequest: async request => {
157
- return this.handleElicitationRequest(request);
158
- },
159
- };
160
- }
161
-
162
- /**
163
- * Handle an elicitation request by sending it to the connected client.
164
- * This method sends an elicitation/create request to the client and waits for the response.
165
- *
166
- * @param request - The elicitation request containing message and schema
167
- * @param serverInstance - Optional server instance to use; defaults to main server for backward compatibility
168
- * @returns Promise that resolves to the client's response
169
- */
170
- private async handleElicitationRequest(
171
- request: ElicitRequest['params'],
172
- serverInstance?: Server,
173
- ): Promise<ElicitResult> {
174
- this.logger.debug(`Sending elicitation request: ${request.message}`);
175
-
176
- const server = serverInstance || this.server;
177
- const response = await server.elicitInput(request);
178
-
179
- this.logger.debug(`Received elicitation response: ${JSON.stringify(response)}`);
180
-
181
- return response;
182
- }
183
-
184
- /**
185
- * Creates a new Server instance configured with all handlers for HTTP sessions.
186
- * Each HTTP client connection gets its own Server instance to avoid routing conflicts.
187
- */
188
- private createServerInstance(): Server {
189
- const capabilities: ServerCapabilities = {
190
- tools: {},
191
- logging: { enabled: true },
192
- elicitation: {},
193
- };
194
-
195
- if (this.resourceOptions) {
196
- capabilities.resources = { subscribe: true, listChanged: true };
197
- }
198
-
199
- if (this.promptOptions) {
200
- capabilities.prompts = { listChanged: true };
201
- }
202
-
203
- const serverInstance = new Server({ name: this.name, version: this.version }, { capabilities });
204
-
205
- // Register all handlers on the new server instance
206
- this.registerHandlersOnServer(serverInstance);
207
-
208
- return serverInstance;
209
- }
210
-
211
- /**
212
- * Registers all MCP handlers on a given server instance.
213
- * This allows us to create multiple server instances with identical functionality.
214
- */
215
- private registerHandlersOnServer(serverInstance: Server) {
216
- // List tools handler
217
- serverInstance.setRequestHandler(ListToolsRequestSchema, async () => {
218
- this.logger.debug('Handling ListTools request');
219
- return {
220
- tools: Object.values(this.convertedTools).map(tool => {
221
- const toolSpec: any = {
222
- name: tool.name,
223
- description: tool.description,
224
- inputSchema: tool.parameters.jsonSchema,
225
- };
226
- if (tool.outputSchema) {
227
- toolSpec.outputSchema = tool.outputSchema.jsonSchema;
228
- }
229
- return toolSpec;
230
- }),
231
- };
232
- });
233
-
234
- // Call tool handler
235
- serverInstance.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
236
- const startTime = Date.now();
237
- try {
238
- const tool = this.convertedTools[request.params.name] as MCPTool;
239
- if (!tool) {
240
- this.logger.warn(`CallTool: Unknown tool '${request.params.name}' requested.`);
241
- return {
242
- content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
243
- isError: true,
244
- };
245
- }
246
-
247
- const validation = tool.parameters.validate?.(request.params.arguments ?? {});
248
- if (validation && !validation.success) {
249
- this.logger.warn(`CallTool: Invalid tool arguments for '${request.params.name}'`, {
250
- errors: validation.error,
251
- });
252
-
253
- // Format validation errors for agent understanding
254
- let errorMessages = 'Validation failed';
255
- if ('errors' in validation.error && Array.isArray(validation.error.errors)) {
256
- errorMessages = validation.error.errors
257
- .map((e: any) => `- ${e.path?.join('.') || 'root'}: ${e.message}`)
258
- .join('\n');
259
- } else if (validation.error instanceof Error) {
260
- errorMessages = validation.error.message;
261
- }
262
-
263
- return {
264
- content: [
265
- {
266
- type: 'text',
267
- text: `Tool validation failed. Please fix the following errors and try again:\n${errorMessages}\n\nProvided arguments: ${JSON.stringify(request.params.arguments, null, 2)}`,
268
- },
269
- ],
270
- isError: true, // Set to true so the LLM sees the error and can self-correct
271
- };
272
- }
273
- if (!tool.execute) {
274
- this.logger.warn(`CallTool: Tool '${request.params.name}' does not have an execute function.`);
275
- return {
276
- content: [{ type: 'text', text: `Tool '${request.params.name}' does not have an execute function.` }],
277
- isError: true,
278
- };
279
- }
280
-
281
- // Create session-aware elicitation for this tool execution
282
- const sessionElicitation = {
283
- sendRequest: async (request: ElicitRequest['params']) => {
284
- return this.handleElicitationRequest(request, serverInstance);
285
- },
286
- };
287
-
288
- const result = await tool.execute(validation?.value ?? request.params.arguments ?? {}, {
289
- messages: [],
290
- toolCallId: '',
291
- elicitation: sessionElicitation,
292
- extra,
293
- });
294
-
295
- this.logger.debug(`CallTool: Tool '${request.params.name}' executed successfully with result:`, result);
296
- const duration = Date.now() - startTime;
297
- this.logger.info(`Tool '${request.params.name}' executed successfully in ${duration}ms.`);
298
-
299
- const response: CallToolResult = { isError: false, content: [] };
300
-
301
- if (tool.outputSchema) {
302
- // Handle both cases: tools that return { structuredContent: ... } and tools that return the plain object
303
- let structuredContent;
304
- if (result && typeof result === 'object' && 'structuredContent' in result) {
305
- // Tool returned { structuredContent: ... } format (MCP-aware tool)
306
- structuredContent = result.structuredContent;
307
- } else {
308
- // Tool returned plain object, wrap it automatically for backward compatibility
309
- structuredContent = result;
310
- }
311
-
312
- const outputValidation = tool.outputSchema.validate?.(structuredContent ?? {});
313
- if (outputValidation && !outputValidation.success) {
314
- this.logger.warn(`CallTool: Invalid structured content for '${request.params.name}'`, {
315
- errors: outputValidation.error,
316
- });
317
- throw new Error(
318
- `Invalid structured content for tool ${request.params.name}: ${JSON.stringify(outputValidation.error)}`,
319
- );
320
- }
321
- response.structuredContent = structuredContent;
322
- }
323
-
324
- if (response.structuredContent) {
325
- response.content = [{ type: 'text', text: JSON.stringify(response.structuredContent) }];
326
- } else {
327
- response.content = [
328
- {
329
- type: 'text',
330
- text: typeof result === 'string' ? result : JSON.stringify(result),
331
- },
332
- ];
333
- }
334
-
335
- return response;
336
- } catch (error) {
337
- const duration = Date.now() - startTime;
338
- if (error instanceof z.ZodError) {
339
- this.logger.warn('Invalid tool arguments', {
340
- tool: request.params.name,
341
- errors: error.errors,
342
- duration: `${duration}ms`,
343
- });
344
- return {
345
- content: [
346
- {
347
- type: 'text',
348
- text: `Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
349
- },
350
- ],
351
- isError: true,
352
- };
353
- }
354
- this.logger.error(`Tool execution failed: ${request.params.name}`, { error });
355
- return {
356
- content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
357
- isError: true,
358
- };
359
- }
360
- });
361
-
362
- // Register resource handlers if resources are configured
363
- if (this.resourceOptions) {
364
- this.registerResourceHandlersOnServer(serverInstance);
365
- }
366
-
367
- // Register prompt handlers if prompts are configured
368
- if (this.promptOptions) {
369
- this.registerPromptHandlersOnServer(serverInstance);
370
- }
371
- }
372
-
373
- /**
374
- * Registers resource-related handlers on a server instance.
375
- */
376
- private registerResourceHandlersOnServer(serverInstance: Server) {
377
- const capturedResourceOptions = this.resourceOptions;
378
- if (!capturedResourceOptions) return;
379
-
380
- // List resources handler
381
- if (capturedResourceOptions.listResources) {
382
- serverInstance.setRequestHandler(ListResourcesRequestSchema, async () => {
383
- this.logger.debug('Handling ListResources request');
384
- if (this.definedResources) {
385
- return { resources: this.definedResources };
386
- } else {
387
- try {
388
- const resources = await capturedResourceOptions.listResources!();
389
- this.definedResources = resources;
390
- this.logger.debug(`Fetched and cached ${this.definedResources.length} resources.`);
391
- return { resources: this.definedResources };
392
- } catch (error) {
393
- this.logger.error('Error fetching resources via listResources():', { error });
394
- throw error;
395
- }
396
- }
397
- });
398
- }
399
-
400
- // Read resource handler
401
- if (capturedResourceOptions.getResourceContent) {
402
- serverInstance.setRequestHandler(ReadResourceRequestSchema, async request => {
403
- const startTime = Date.now();
404
- const uri = request.params.uri;
405
- this.logger.debug(`Handling ReadResource request for URI: ${uri}`);
406
-
407
- if (!this.definedResources) {
408
- const resources = await this.resourceOptions?.listResources?.();
409
- if (!resources) throw new Error('Failed to load resources');
410
- this.definedResources = resources;
411
- }
412
-
413
- const resource = this.definedResources?.find(r => r.uri === uri);
414
-
415
- if (!resource) {
416
- this.logger.warn(`ReadResource: Unknown resource URI '${uri}' requested.`);
417
- throw new Error(`Resource not found: ${uri}`);
418
- }
419
-
420
- try {
421
- const resourcesOrResourceContent = await capturedResourceOptions.getResourceContent({ uri });
422
- const resourcesContent = Array.isArray(resourcesOrResourceContent)
423
- ? resourcesOrResourceContent
424
- : [resourcesOrResourceContent];
425
- const contents: ResourceContents[] = resourcesContent.map(resourceContent => {
426
- const contentItem: ResourceContents = {
427
- uri: resource.uri,
428
- mimeType: resource.mimeType,
429
- };
430
- if ('text' in resourceContent) {
431
- contentItem.text = resourceContent.text;
432
- }
433
-
434
- if ('blob' in resourceContent) {
435
- contentItem.blob = resourceContent.blob;
436
- }
437
-
438
- return contentItem;
439
- });
440
- const duration = Date.now() - startTime;
441
- this.logger.info(`Resource '${uri}' read successfully in ${duration}ms.`);
442
- return {
443
- contents,
444
- };
445
- } catch (error) {
446
- const duration = Date.now() - startTime;
447
- this.logger.error(`Failed to get content for resource URI '${uri}' in ${duration}ms`, { error });
448
- throw error;
449
- }
450
- });
451
- }
452
-
453
- // Resource templates handler
454
- if (capturedResourceOptions.resourceTemplates) {
455
- serverInstance.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
456
- this.logger.debug('Handling ListResourceTemplates request');
457
- if (this.definedResourceTemplates) {
458
- return { resourceTemplates: this.definedResourceTemplates };
459
- } else {
460
- try {
461
- const templates = await capturedResourceOptions.resourceTemplates!();
462
- this.definedResourceTemplates = templates;
463
- this.logger.debug(`Fetched and cached ${this.definedResourceTemplates.length} resource templates.`);
464
- return { resourceTemplates: this.definedResourceTemplates };
465
- } catch (error) {
466
- this.logger.error('Error fetching resource templates via resourceTemplates():', { error });
467
- throw error;
468
- }
469
- }
470
- });
471
- }
472
-
473
- // Subscribe/unsubscribe handlers
474
- serverInstance.setRequestHandler(SubscribeRequestSchema, async (request: { params: { uri: string } }) => {
475
- const uri = request.params.uri;
476
- this.logger.info(`Received resources/subscribe request for URI: ${uri}`);
477
- this.subscriptions.add(uri);
478
- return {};
479
- });
480
-
481
- serverInstance.setRequestHandler(UnsubscribeRequestSchema, async (request: { params: { uri: string } }) => {
482
- const uri = request.params.uri;
483
- this.logger.info(`Received resources/unsubscribe request for URI: ${uri}`);
484
- this.subscriptions.delete(uri);
485
- return {};
486
- });
487
- }
488
-
489
- /**
490
- * Registers prompt-related handlers on a server instance.
491
- */
492
- private registerPromptHandlersOnServer(serverInstance: Server) {
493
- const capturedPromptOptions = this.promptOptions;
494
- if (!capturedPromptOptions) return;
495
-
496
- // List prompts handler
497
- if (capturedPromptOptions.listPrompts) {
498
- serverInstance.setRequestHandler(ListPromptsRequestSchema, async () => {
499
- this.logger.debug('Handling ListPrompts request');
500
- if (this.definedPrompts) {
501
- return {
502
- prompts: this.definedPrompts?.map(p => ({ ...p, version: p.version ?? undefined })),
503
- };
504
- } else {
505
- try {
506
- const prompts = await capturedPromptOptions.listPrompts();
507
- for (const prompt of prompts) {
508
- PromptSchema.parse(prompt);
509
- }
510
- this.definedPrompts = prompts;
511
- this.logger.debug(`Fetched and cached ${this.definedPrompts.length} prompts.`);
512
- return {
513
- prompts: this.definedPrompts?.map(p => ({ ...p, version: p.version ?? undefined })),
514
- };
515
- } catch (error) {
516
- this.logger.error('Error fetching prompts via listPrompts():', {
517
- error: error instanceof Error ? error.message : String(error),
518
- });
519
- throw error;
520
- }
521
- }
522
- });
523
- }
524
-
525
- // Get prompt handler
526
- if (capturedPromptOptions.getPromptMessages) {
527
- serverInstance.setRequestHandler(
528
- GetPromptRequestSchema,
529
- async (request: { params: { name: string; version?: string; arguments?: any } }) => {
530
- const startTime = Date.now();
531
- const { name, version, arguments: args } = request.params;
532
- if (!this.definedPrompts) {
533
- const prompts = await this.promptOptions?.listPrompts?.();
534
- if (!prompts) throw new Error('Failed to load prompts');
535
- this.definedPrompts = prompts;
536
- }
537
- // Select prompt by name and version (if provided)
538
- let prompt;
539
- if (version) {
540
- prompt = this.definedPrompts?.find(p => p.name === name && p.version === version);
541
- } else {
542
- // Select the first matching name if no version is provided.
543
- prompt = this.definedPrompts?.find(p => p.name === name);
544
- }
545
- if (!prompt) throw new Error(`Prompt "${name}"${version ? ` (version ${version})` : ''} not found`);
546
- // Validate required arguments
547
- if (prompt.arguments) {
548
- for (const arg of prompt.arguments) {
549
- if (arg.required && (args?.[arg.name] === undefined || args?.[arg.name] === null)) {
550
- throw new Error(`Missing required argument: ${arg.name}`);
551
- }
552
- }
553
- }
554
- try {
555
- let messages: any[] = [];
556
- if (capturedPromptOptions.getPromptMessages) {
557
- messages = await capturedPromptOptions.getPromptMessages({ name, version, args });
558
- }
559
- const duration = Date.now() - startTime;
560
- this.logger.info(
561
- `Prompt '${name}'${version ? ` (version ${version})` : ''} retrieved successfully in ${duration}ms.`,
562
- );
563
- return { prompt, messages };
564
- } catch (error) {
565
- const duration = Date.now() - startTime;
566
- this.logger.error(`Failed to get content for prompt '${name}' in ${duration}ms`, { error });
567
- throw error;
568
- }
569
- },
570
- );
571
- }
572
- }
573
-
574
- private convertAgentsToTools(
575
- agentsConfig?: Record<string, Agent>,
576
- definedConvertedTools?: Record<string, ConvertedTool>,
577
- ): Record<string, ConvertedTool> {
578
- const agentTools: Record<string, ConvertedTool> = {};
579
- if (!agentsConfig) {
580
- return agentTools;
581
- }
582
-
583
- for (const agentKey in agentsConfig) {
584
- const agent = agentsConfig[agentKey];
585
- if (!agent || !('generate' in agent)) {
586
- this.logger.warn(`Agent instance for '${agentKey}' is invalid or missing a generate function. Skipping.`);
587
- continue;
588
- }
589
-
590
- const agentDescription = agent.getDescription();
591
-
592
- if (!agentDescription) {
593
- throw new Error(
594
- `Agent '${agent.name}' (key: '${agentKey}') must have a non-empty description to be used in an MCPServer.`,
595
- );
596
- }
597
-
598
- const agentToolName = `ask_${agentKey}`;
599
- if (definedConvertedTools?.[agentToolName] || agentTools[agentToolName]) {
600
- this.logger.warn(
601
- `Tool with name '${agentToolName}' already exists. Agent '${agentKey}' will not be added as a duplicate tool.`,
602
- );
603
- continue;
604
- }
605
-
606
- const agentToolDefinition = createTool({
607
- id: agentToolName,
608
- description: `Ask agent '${agent.name}' a question. Agent description: ${agentDescription}`,
609
- inputSchema: z.object({
610
- message: z.string().describe('The question or input for the agent.'),
611
- }),
612
- execute: async ({ context, runtimeContext }) => {
613
- this.logger.debug(
614
- `Executing agent tool '${agentToolName}' for agent '${agent.name}' with message: "${context.message}"`,
615
- );
616
- try {
617
- const response = await agent.generate(context.message, { runtimeContext });
618
- return response;
619
- } catch (error) {
620
- this.logger.error(`Error executing agent tool '${agentToolName}' for agent '${agent.name}':`, error);
621
- throw error;
622
- }
623
- },
624
- });
625
-
626
- const options = {
627
- name: agentToolName,
628
- logger: this.logger,
629
- mastra: this.mastra,
630
- runtimeContext: new RuntimeContext(),
631
- description: agentToolDefinition.description,
632
- };
633
- const coreTool = makeCoreTool(agentToolDefinition, options) as InternalCoreTool;
634
-
635
- agentTools[agentToolName] = {
636
- name: agentToolName,
637
- description: coreTool.description,
638
- parameters: coreTool.parameters,
639
- execute: coreTool.execute!,
640
- toolType: 'agent',
641
- };
642
- this.logger.info(`Registered agent '${agent.name}' (key: '${agentKey}') as tool: '${agentToolName}'`);
643
- }
644
- return agentTools;
645
- }
646
-
647
- private convertWorkflowsToTools(
648
- workflowsConfig?: Record<string, Workflow>,
649
- definedConvertedTools?: Record<string, ConvertedTool>,
650
- ): Record<string, ConvertedTool> {
651
- const workflowTools: Record<string, ConvertedTool> = {};
652
- if (!workflowsConfig) {
653
- return workflowTools;
654
- }
655
-
656
- for (const workflowKey in workflowsConfig) {
657
- const workflow = workflowsConfig[workflowKey];
658
- if (!workflow || typeof workflow.createRun !== 'function') {
659
- this.logger.warn(
660
- `Workflow instance for '${workflowKey}' is invalid or missing a createRun function. Skipping.`,
661
- );
662
- continue;
663
- }
664
-
665
- const workflowDescription = workflow.description;
666
- if (!workflowDescription) {
667
- throw new Error(
668
- `Workflow '${workflow.id}' (key: '${workflowKey}') must have a non-empty description to be used in an MCPServer.`,
669
- );
670
- }
671
-
672
- const workflowToolName = `run_${workflowKey}`;
673
- if (definedConvertedTools?.[workflowToolName] || workflowTools[workflowToolName]) {
674
- this.logger.warn(
675
- `Tool with name '${workflowToolName}' already exists. Workflow '${workflowKey}' will not be added as a duplicate tool.`,
676
- );
677
- continue;
678
- }
679
-
680
- const workflowToolDefinition = createTool({
681
- id: workflowToolName,
682
- description: `Run workflow '${workflowKey}'. Workflow description: ${workflowDescription}`,
683
- inputSchema: workflow.inputSchema,
684
- execute: async ({ context, runtimeContext }) => {
685
- this.logger.debug(
686
- `Executing workflow tool '${workflowToolName}' for workflow '${workflow.id}' with input:`,
687
- context,
688
- );
689
- try {
690
- const run = workflow.createRun({ runId: runtimeContext?.get('runId') });
691
-
692
- const response = await run.start({ inputData: context, runtimeContext });
693
-
694
- return response;
695
- } catch (error) {
696
- this.logger.error(
697
- `Error executing workflow tool '${workflowToolName}' for workflow '${workflow.id}':`,
698
- error,
699
- );
700
- throw error;
701
- }
702
- },
703
- });
704
-
705
- const options = {
706
- name: workflowToolName,
707
- logger: this.logger,
708
- mastra: this.mastra,
709
- runtimeContext: new RuntimeContext(),
710
- description: workflowToolDefinition.description,
711
- };
712
-
713
- const coreTool = makeCoreTool(workflowToolDefinition, options) as InternalCoreTool;
714
-
715
- workflowTools[workflowToolName] = {
716
- name: workflowToolName,
717
- description: coreTool.description,
718
- parameters: coreTool.parameters,
719
- outputSchema: coreTool.outputSchema,
720
- execute: coreTool.execute!,
721
- toolType: 'workflow',
722
- };
723
- this.logger.info(`Registered workflow '${workflow.id}' (key: '${workflowKey}') as tool: '${workflowToolName}'`);
724
- }
725
- return workflowTools;
726
- }
727
-
728
- /**
729
- * Convert and validate all provided tools, logging registration status.
730
- * Also converts agents and workflows into tools.
731
- * @param tools Tool definitions
732
- * @param agentsConfig Agent definitions to be converted to tools, expected from MCPServerConfig
733
- * @param workflowsConfig Workflow definitions to be converted to tools, expected from MCPServerConfig
734
- * @returns Converted tools registry
735
- */
736
- convertTools(
737
- tools: ToolsInput,
738
- agentsConfig?: Record<string, Agent>,
739
- workflowsConfig?: Record<string, Workflow>,
740
- ): Record<string, ConvertedTool> {
741
- const definedConvertedTools: Record<string, ConvertedTool> = {};
742
-
743
- for (const toolName of Object.keys(tools)) {
744
- const toolInstance = tools[toolName];
745
- if (!toolInstance) {
746
- this.logger.warn(`Tool instance for '${toolName}' is undefined. Skipping.`);
747
- continue;
748
- }
749
-
750
- if (typeof toolInstance.execute !== 'function') {
751
- this.logger.warn(`Tool '${toolName}' does not have a valid execute function. Skipping.`);
752
- continue;
753
- }
754
-
755
- const options = {
756
- name: toolName,
757
- runtimeContext: new RuntimeContext(),
758
- mastra: this.mastra,
759
- logger: this.logger,
760
- description: toolInstance?.description,
761
- };
762
-
763
- const coreTool = makeCoreTool(toolInstance, options) as InternalCoreTool;
764
-
765
- definedConvertedTools[toolName] = {
766
- name: toolName,
767
- description: coreTool.description,
768
- parameters: coreTool.parameters,
769
- outputSchema: coreTool.outputSchema,
770
- execute: coreTool.execute!,
771
- };
772
- this.logger.info(`Registered explicit tool: '${toolName}'`);
773
- }
774
- this.logger.info(`Total defined tools registered: ${Object.keys(definedConvertedTools).length}`);
775
-
776
- let agentDerivedTools: Record<string, ConvertedTool> = {};
777
- let workflowDerivedTools: Record<string, ConvertedTool> = {};
778
- try {
779
- agentDerivedTools = this.convertAgentsToTools(agentsConfig, definedConvertedTools);
780
- workflowDerivedTools = this.convertWorkflowsToTools(workflowsConfig, definedConvertedTools);
781
- } catch (e) {
782
- const mastraError = new MastraError(
783
- {
784
- id: 'MCP_SERVER_AGENT_OR_WORKFLOW_TOOL_CONVERSION_FAILED',
785
- domain: ErrorDomain.MCP,
786
- category: ErrorCategory.USER,
787
- },
788
- e,
789
- );
790
- this.logger.trackException(mastraError);
791
- this.logger.error('Failed to convert tools:', {
792
- error: mastraError.toString(),
793
- });
794
- throw mastraError;
795
- }
796
-
797
- const allConvertedTools = { ...definedConvertedTools, ...agentDerivedTools, ...workflowDerivedTools };
798
-
799
- const finalToolCount = Object.keys(allConvertedTools).length;
800
- const definedCount = Object.keys(definedConvertedTools).length;
801
- const fromAgentsCount = Object.keys(agentDerivedTools).length;
802
- const fromWorkflowsCount = Object.keys(workflowDerivedTools).length;
803
- this.logger.info(
804
- `${finalToolCount} total tools registered (${definedCount} defined + ${fromAgentsCount} agents + ${fromWorkflowsCount} workflows)`,
805
- );
806
-
807
- return allConvertedTools;
808
- }
809
-
810
- /**
811
- * Start the MCP server using stdio transport (for Windsurf integration).
812
- */
813
- public async startStdio(): Promise<void> {
814
- this.stdioTransport = new StdioServerTransport();
815
- try {
816
- await this.server.connect(this.stdioTransport);
817
- } catch (error) {
818
- const mastraError = new MastraError(
819
- {
820
- id: 'MCP_SERVER_STDIO_CONNECTION_FAILED',
821
- domain: ErrorDomain.MCP,
822
- category: ErrorCategory.THIRD_PARTY,
823
- },
824
- error,
825
- );
826
- this.logger.trackException(mastraError);
827
- this.logger.error('Failed to connect MCP server using stdio transport:', {
828
- error: mastraError.toString(),
829
- });
830
- throw mastraError;
831
- }
832
- this.logger.info('Started MCP Server (stdio)');
833
- }
834
-
835
- /**
836
- * Handles MCP-over-SSE protocol for user-provided HTTP servers.
837
- * Call this from your HTTP server for both the SSE and message endpoints.
838
- *
839
- * @param url Parsed URL of the incoming request
840
- * @param ssePath Path for establishing the SSE connection (e.g. '/sse')
841
- * @param messagePath Path for POSTing client messages (e.g. '/message')
842
- * @param req Incoming HTTP request
843
- * @param res HTTP response (must support .write/.end)
844
- */
845
- public async startSSE({ url, ssePath, messagePath, req, res }: MCPServerSSEOptions): Promise<void> {
846
- try {
847
- if (url.pathname === ssePath) {
848
- await this.connectSSE({
849
- messagePath,
850
- res,
851
- });
852
- } else if (url.pathname === messagePath) {
853
- this.logger.debug('Received message');
854
- if (!this.sseTransport) {
855
- res.writeHead(503);
856
- res.end('SSE connection not established');
857
- return;
858
- }
859
- await this.sseTransport.handlePostMessage(req, res);
860
- } else {
861
- this.logger.debug('Unknown path:', { path: url.pathname });
862
- res.writeHead(404);
863
- res.end();
864
- }
865
- } catch (e) {
866
- const mastraError = new MastraError(
867
- {
868
- id: 'MCP_SERVER_SSE_START_FAILED',
869
- domain: ErrorDomain.MCP,
870
- category: ErrorCategory.USER,
871
- details: {
872
- url: url.toString(),
873
- ssePath,
874
- messagePath,
875
- },
876
- },
877
- e,
878
- );
879
- this.logger.trackException(mastraError);
880
- this.logger.error('Failed to start MCP Server (SSE):', { error: mastraError.toString() });
881
- throw mastraError;
882
- }
883
- }
884
-
885
- /**
886
- * Handles MCP-over-SSE protocol for user-provided HTTP servers.
887
- * Call this from your HTTP server for both the SSE and message endpoints.
888
- *
889
- * @param url Parsed URL of the incoming request
890
- * @param ssePath Path for establishing the SSE connection (e.g. '/sse')
891
- * @param messagePath Path for POSTing client messages (e.g. '/message')
892
- * @param context Incoming Hono context
893
- */
894
- public async startHonoSSE({ url, ssePath, messagePath, context }: MCPServerHonoSSEOptions) {
895
- try {
896
- if (url.pathname === ssePath) {
897
- return streamSSE(context, async stream => {
898
- await this.connectHonoSSE({
899
- messagePath,
900
- stream,
901
- });
902
- });
903
- } else if (url.pathname === messagePath) {
904
- this.logger.debug('Received message');
905
- const sessionId = context.req.query('sessionId');
906
- this.logger.debug('Received message for sessionId', { sessionId });
907
- if (!sessionId) {
908
- return context.text('No sessionId provided', 400);
909
- }
910
- if (!this.sseHonoTransports.has(sessionId)) {
911
- return context.text(`No transport found for sessionId ${sessionId}`, 400);
912
- }
913
- const message = await this.sseHonoTransports.get(sessionId)?.handlePostMessage(context);
914
- if (!message) {
915
- return context.text('Transport not found', 400);
916
- }
917
- return message;
918
- } else {
919
- this.logger.debug('Unknown path:', { path: url.pathname });
920
- return context.text('Unknown path', 404);
921
- }
922
- } catch (e) {
923
- const mastraError = new MastraError(
924
- {
925
- id: 'MCP_SERVER_HONO_SSE_START_FAILED',
926
- domain: ErrorDomain.MCP,
927
- category: ErrorCategory.USER,
928
- details: {
929
- url: url.toString(),
930
- ssePath,
931
- messagePath,
932
- },
933
- },
934
- e,
935
- );
936
- this.logger.trackException(mastraError);
937
- this.logger.error('Failed to start MCP Server (Hono SSE):', { error: mastraError.toString() });
938
- throw mastraError;
939
- }
940
- }
941
-
942
- /**
943
- * Handles MCP-over-StreamableHTTP protocol for user-provided HTTP servers.
944
- * Call this from your HTTP server for the streamable HTTP endpoint.
945
- *
946
- * @param url Parsed URL of the incoming request
947
- * @param httpPath Path for establishing the streamable HTTP connection (e.g. '/mcp')
948
- * @param req Incoming HTTP request
949
- * @param res HTTP response (must support .write/.end)
950
- * @param options Optional options to pass to the transport (e.g. sessionIdGenerator)
951
- */
952
- public async startHTTP({
953
- url,
954
- httpPath,
955
- req,
956
- res,
957
- options = { sessionIdGenerator: () => randomUUID() },
958
- }: {
959
- url: URL;
960
- httpPath: string;
961
- req: http.IncomingMessage;
962
- res: http.ServerResponse<http.IncomingMessage>;
963
- options?: StreamableHTTPServerTransportOptions;
964
- }) {
965
- this.logger.debug(`startHTTP: Received ${req.method} request to ${url.pathname}`);
966
-
967
- if (url.pathname !== httpPath) {
968
- this.logger.debug(`startHTTP: Pathname ${url.pathname} does not match httpPath ${httpPath}. Returning 404.`);
969
- res.writeHead(404);
970
- res.end();
971
- return;
972
- }
973
-
974
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
975
- let transport: StreamableHTTPServerTransport | undefined;
976
-
977
- this.logger.debug(
978
- `startHTTP: Session ID from headers: ${sessionId}. Active transports: ${Array.from(this.streamableHTTPTransports.keys()).join(', ')}`,
979
- );
980
-
981
- try {
982
- if (sessionId && this.streamableHTTPTransports.has(sessionId)) {
983
- // Found existing session
984
- transport = this.streamableHTTPTransports.get(sessionId)!;
985
- this.logger.debug(`startHTTP: Using existing Streamable HTTP transport for session ID: ${sessionId}`);
986
-
987
- if (req.method === 'GET') {
988
- this.logger.debug(
989
- `startHTTP: Handling GET request for existing session ${sessionId}. Calling transport.handleRequest.`,
990
- );
991
- }
992
-
993
- // Handle the request using the existing transport
994
- // Need to parse body for POST requests before passing to handleRequest
995
- const body =
996
- req.method === 'POST'
997
- ? await new Promise((resolve, reject) => {
998
- let data = '';
999
- req.on('data', chunk => (data += chunk));
1000
- req.on('end', () => {
1001
- try {
1002
- resolve(JSON.parse(data));
1003
- } catch (e) {
1004
- reject(e);
1005
- }
1006
- });
1007
- req.on('error', reject);
1008
- })
1009
- : undefined;
1010
-
1011
- await transport.handleRequest(req, res, body);
1012
- } else {
1013
- // No session ID or session ID not found
1014
- this.logger.debug(`startHTTP: No existing Streamable HTTP session ID found. ${req.method}`);
1015
-
1016
- // Only allow new sessions via POST initialize request
1017
- if (req.method === 'POST') {
1018
- const body = await new Promise((resolve, reject) => {
1019
- let data = '';
1020
- req.on('data', chunk => (data += chunk));
1021
- req.on('end', () => {
1022
- try {
1023
- resolve(JSON.parse(data));
1024
- } catch (e) {
1025
- reject(e);
1026
- }
1027
- });
1028
- req.on('error', reject);
1029
- });
1030
-
1031
- // Import isInitializeRequest from the correct path
1032
- const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
1033
-
1034
- if (isInitializeRequest(body)) {
1035
- this.logger.debug('startHTTP: Received Streamable HTTP initialize request, creating new transport.');
1036
-
1037
- // Create a new transport for the new session
1038
- transport = new StreamableHTTPServerTransport({
1039
- ...options,
1040
- sessionIdGenerator: () => randomUUID(),
1041
- onsessioninitialized: id => {
1042
- this.streamableHTTPTransports.set(id, transport!);
1043
- },
1044
- });
1045
-
1046
- // Set up onclose handler to clean up transport when closed
1047
- transport.onclose = () => {
1048
- const closedSessionId = transport?.sessionId;
1049
- if (closedSessionId && this.streamableHTTPTransports.has(closedSessionId)) {
1050
- this.logger.debug(
1051
- `startHTTP: Streamable HTTP transport closed for session ${closedSessionId}, removing from map.`,
1052
- );
1053
- this.streamableHTTPTransports.delete(closedSessionId);
1054
- // Also clean up the server instance for this session
1055
- if (this.httpServerInstances.has(closedSessionId)) {
1056
- this.httpServerInstances.delete(closedSessionId);
1057
- this.logger.debug(`startHTTP: Cleaned up server instance for closed session ${closedSessionId}`);
1058
- }
1059
- }
1060
- };
1061
-
1062
- // Create a new server instance for this HTTP session
1063
- const sessionServerInstance = this.createServerInstance();
1064
-
1065
- // Connect the new server instance to the new transport
1066
- await sessionServerInstance.connect(transport);
1067
-
1068
- // Store both the transport and server instance when the session is initialized
1069
- if (transport.sessionId) {
1070
- this.streamableHTTPTransports.set(transport.sessionId, transport);
1071
- this.httpServerInstances.set(transport.sessionId, sessionServerInstance);
1072
- this.logger.debug(
1073
- `startHTTP: Streamable HTTP session initialized and stored with ID: ${transport.sessionId}`,
1074
- );
1075
- } else {
1076
- this.logger.warn('startHTTP: Streamable HTTP transport initialized without a session ID.');
1077
- }
1078
-
1079
- // Handle the initialize request
1080
- return await transport.handleRequest(req, res, body);
1081
- } else {
1082
- // POST request but not initialize, and no session ID
1083
- this.logger.warn('startHTTP: Received non-initialize POST request without a session ID.');
1084
- res.writeHead(400, { 'Content-Type': 'application/json' });
1085
- res.end(
1086
- JSON.stringify({
1087
- jsonrpc: '2.0',
1088
- error: {
1089
- code: -32000,
1090
- message: 'Bad Request: No valid session ID provided for non-initialize request',
1091
- },
1092
- id: (body as any)?.id ?? null, // Include original request ID if available
1093
- }),
1094
- );
1095
- }
1096
- } else {
1097
- // Non-POST request (GET/DELETE) without a session ID
1098
- this.logger.warn(`startHTTP: Received ${req.method} request without a session ID.`);
1099
- res.writeHead(400, { 'Content-Type': 'application/json' });
1100
- res.end(
1101
- JSON.stringify({
1102
- jsonrpc: '2.0',
1103
- error: {
1104
- code: -32000,
1105
- message: `Bad Request: ${req.method} request requires a valid session ID`,
1106
- },
1107
- id: null,
1108
- }),
1109
- );
1110
- }
1111
- }
1112
- } catch (error) {
1113
- const mastraError = new MastraError(
1114
- {
1115
- id: 'MCP_SERVER_HTTP_CONNECTION_FAILED',
1116
- domain: ErrorDomain.MCP,
1117
- category: ErrorCategory.USER,
1118
- text: 'Failed to connect MCP server using HTTP transport',
1119
- },
1120
- error,
1121
- );
1122
- this.logger.trackException(mastraError);
1123
- this.logger.error('startHTTP: Error handling Streamable HTTP request:', { error: mastraError });
1124
- // If headers haven't been sent, send an error response
1125
- if (!res.headersSent) {
1126
- res.writeHead(500, { 'Content-Type': 'application/json' });
1127
- res.end(
1128
- JSON.stringify({
1129
- jsonrpc: '2.0',
1130
- error: {
1131
- code: -32603,
1132
- message: 'Internal server error',
1133
- },
1134
- id: null, // Cannot determine original request ID in catch
1135
- }),
1136
- );
1137
- }
1138
- }
1139
- }
1140
-
1141
- public async connectSSE({
1142
- messagePath,
1143
- res,
1144
- }: {
1145
- messagePath: string;
1146
- res: http.ServerResponse<http.IncomingMessage>;
1147
- }) {
1148
- try {
1149
- this.logger.debug('Received SSE connection');
1150
- this.sseTransport = new SSEServerTransport(messagePath, res);
1151
- await this.server.connect(this.sseTransport);
1152
-
1153
- this.server.onclose = async () => {
1154
- this.sseTransport = undefined;
1155
- await this.server.close();
1156
- };
1157
-
1158
- res.on('close', () => {
1159
- this.sseTransport = undefined;
1160
- });
1161
- } catch (e) {
1162
- const mastraError = new MastraError(
1163
- {
1164
- id: 'MCP_SERVER_SSE_CONNECT_FAILED',
1165
- domain: ErrorDomain.MCP,
1166
- category: ErrorCategory.USER,
1167
- details: {
1168
- messagePath,
1169
- },
1170
- },
1171
- e,
1172
- );
1173
- this.logger.trackException(mastraError);
1174
- this.logger.error('Failed to connect to MCP Server (SSE):', { error: mastraError });
1175
- throw mastraError;
1176
- }
1177
- }
1178
-
1179
- public async connectHonoSSE({ messagePath, stream }: { messagePath: string; stream: SSEStreamingApi }) {
1180
- this.logger.debug('Received SSE connection');
1181
- const sseTransport = new SSETransport(messagePath, stream);
1182
- const sessionId = sseTransport.sessionId;
1183
- this.logger.debug('SSE Transport created with sessionId:', { sessionId });
1184
- this.sseHonoTransports.set(sessionId, sseTransport);
1185
-
1186
- stream.onAbort(() => {
1187
- this.logger.debug('SSE Transport aborted with sessionId:', { sessionId });
1188
- this.sseHonoTransports.delete(sessionId);
1189
- });
1190
- try {
1191
- await this.server.connect(sseTransport);
1192
- this.server.onclose = async () => {
1193
- this.logger.debug('SSE Transport closed with sessionId:', { sessionId });
1194
- this.sseHonoTransports.delete(sessionId);
1195
- await this.server.close();
1196
- };
1197
-
1198
- while (true) {
1199
- // This will keep the connection alive
1200
- // You can also await for a promise that never resolves
1201
- await stream.sleep(60_000);
1202
- const sessionIds = Array.from(this.sseHonoTransports.keys() || []);
1203
- this.logger.debug('Active Hono SSE sessions:', { sessionIds });
1204
- await stream.write(':keep-alive\n\n');
1205
- }
1206
- } catch (e) {
1207
- const mastraError = new MastraError(
1208
- {
1209
- id: 'MCP_SERVER_HONO_SSE_CONNECT_FAILED',
1210
- domain: ErrorDomain.MCP,
1211
- category: ErrorCategory.USER,
1212
- details: {
1213
- messagePath,
1214
- },
1215
- },
1216
- e,
1217
- );
1218
- this.logger.trackException(mastraError);
1219
- this.logger.error('Failed to connect to MCP Server (Hono SSE):', { error: mastraError });
1220
- throw mastraError;
1221
- }
1222
- }
1223
-
1224
- /**
1225
- * Close the MCP server and all its connections
1226
- */
1227
- async close() {
1228
- try {
1229
- if (this.stdioTransport) {
1230
- await this.stdioTransport.close?.();
1231
- this.stdioTransport = undefined;
1232
- }
1233
- if (this.sseTransport) {
1234
- await this.sseTransport.close?.();
1235
- this.sseTransport = undefined;
1236
- }
1237
- if (this.sseHonoTransports) {
1238
- for (const transport of this.sseHonoTransports.values()) {
1239
- await transport.close?.();
1240
- }
1241
- this.sseHonoTransports.clear();
1242
- }
1243
- // Close all active Streamable HTTP transports and their server instances
1244
- if (this.streamableHTTPTransports) {
1245
- for (const transport of this.streamableHTTPTransports.values()) {
1246
- await transport.close?.();
1247
- }
1248
- this.streamableHTTPTransports.clear();
1249
- }
1250
- // Close all HTTP server instances
1251
- if (this.httpServerInstances) {
1252
- for (const serverInstance of this.httpServerInstances.values()) {
1253
- await serverInstance.close?.();
1254
- }
1255
- this.httpServerInstances.clear();
1256
- }
1257
- await this.server.close();
1258
- this.logger.info('MCP server closed.');
1259
- } catch (error) {
1260
- const mastraError = new MastraError(
1261
- {
1262
- id: 'MCP_SERVER_CLOSE_FAILED',
1263
- domain: ErrorDomain.MCP,
1264
- category: ErrorCategory.THIRD_PARTY,
1265
- },
1266
- error,
1267
- );
1268
- this.logger.trackException(mastraError);
1269
- this.logger.error('Error closing MCP server:', { error: mastraError });
1270
- throw mastraError;
1271
- }
1272
- }
1273
-
1274
- /**
1275
- * Gets the basic information about the server, conforming to the Server schema.
1276
- * @returns ServerInfo object.
1277
- */
1278
- public getServerInfo(): ServerInfo {
1279
- return {
1280
- id: this.id,
1281
- name: this.name,
1282
- description: this.description,
1283
- repository: this.repository,
1284
- version_detail: {
1285
- version: this.version,
1286
- release_date: this.releaseDate,
1287
- is_latest: this.isLatest,
1288
- },
1289
- };
1290
- }
1291
-
1292
- /**
1293
- * Gets detailed information about the server, conforming to the ServerDetail schema.
1294
- * @returns ServerDetailInfo object.
1295
- */
1296
- public getServerDetail(): ServerDetailInfo {
1297
- return {
1298
- ...this.getServerInfo(),
1299
- package_canonical: this.packageCanonical,
1300
- packages: this.packages,
1301
- remotes: this.remotes,
1302
- };
1303
- }
1304
-
1305
- /**
1306
- * Gets a list of tools provided by this MCP server, including their schemas.
1307
- * This leverages the same tool information used by the internal ListTools MCP request.
1308
- * @returns An object containing an array of tool information.
1309
- */
1310
- public getToolListInfo(): {
1311
- tools: Array<{ name: string; description?: string; inputSchema: any; outputSchema?: any; toolType?: MCPToolType }>;
1312
- } {
1313
- this.logger.debug(`Getting tool list information for MCPServer '${this.name}'`);
1314
- return {
1315
- tools: Object.entries(this.convertedTools).map(([toolId, tool]) => ({
1316
- id: toolId,
1317
- name: tool.name,
1318
- description: tool.description,
1319
- inputSchema: tool.parameters?.jsonSchema || tool.parameters,
1320
- outputSchema: tool.outputSchema?.jsonSchema || tool.outputSchema,
1321
- toolType: tool.toolType,
1322
- })),
1323
- };
1324
- }
1325
-
1326
- /**
1327
- * Gets information for a specific tool provided by this MCP server.
1328
- * @param toolId The ID/name of the tool to retrieve.
1329
- * @returns Tool information (name, description, inputSchema) or undefined if not found.
1330
- */
1331
- public getToolInfo(
1332
- toolId: string,
1333
- ): { name: string; description?: string; inputSchema: any; outputSchema?: any; toolType?: MCPToolType } | undefined {
1334
- const tool = this.convertedTools[toolId];
1335
- if (!tool) {
1336
- this.logger.debug(`Tool '${toolId}' not found on MCPServer '${this.name}'`);
1337
- return undefined;
1338
- }
1339
- this.logger.debug(`Getting info for tool '${toolId}' on MCPServer '${this.name}'`);
1340
- return {
1341
- name: tool.name,
1342
- description: tool.description,
1343
- inputSchema: tool.parameters?.jsonSchema || tool.parameters,
1344
- outputSchema: tool.outputSchema?.jsonSchema || tool.outputSchema,
1345
- toolType: tool.toolType,
1346
- };
1347
- }
1348
-
1349
- /**
1350
- * Executes a specific tool provided by this MCP server.
1351
- * @param toolId The ID/name of the tool to execute.
1352
- * @param args The arguments to pass to the tool's execute function.
1353
- * @param executionContext Optional context for the tool execution.
1354
- * @returns A promise that resolves to the result of the tool execution.
1355
- * @throws Error if the tool is not found, validation fails, or execution fails.
1356
- */
1357
- public async executeTool(
1358
- toolId: string,
1359
- args: any,
1360
- executionContext?: { messages?: any[]; toolCallId?: string },
1361
- ): Promise<any> {
1362
- const tool = this.convertedTools[toolId];
1363
- let validatedArgs = args;
1364
- try {
1365
- if (!tool) {
1366
- this.logger.warn(`ExecuteTool: Unknown tool '${toolId}' requested on MCPServer '${this.name}'.`);
1367
- throw new Error(`Unknown tool: ${toolId}`);
1368
- }
1369
-
1370
- this.logger.debug(`ExecuteTool: Invoking '${toolId}' with arguments:`, args);
1371
-
1372
- if (tool.parameters instanceof z.ZodType && typeof tool.parameters.safeParse === 'function') {
1373
- const validation = tool.parameters.safeParse(args ?? {});
1374
- if (!validation.success) {
1375
- const errorMessages = validation.error.errors
1376
- .map((e: z.ZodIssue) => `- ${e.path?.join('.') || 'root'}: ${e.message}`)
1377
- .join('\n');
1378
- this.logger.warn(`ExecuteTool: Invalid tool arguments for '${toolId}': ${errorMessages}`, {
1379
- errors: validation.error.format(),
1380
- });
1381
- // Return validation error as a result instead of throwing
1382
- return {
1383
- error: true,
1384
- message: `Tool validation failed. Please fix the following errors and try again:\n${errorMessages}\n\nProvided arguments: ${JSON.stringify(args, null, 2)}`,
1385
- validationErrors: validation.error.format(),
1386
- };
1387
- }
1388
- validatedArgs = validation.data;
1389
- } else {
1390
- this.logger.debug(
1391
- `ExecuteTool: Tool '${toolId}' parameters is not a Zod schema with safeParse or is undefined. Skipping validation.`,
1392
- );
1393
- }
1394
-
1395
- if (!tool.execute) {
1396
- this.logger.error(`ExecuteTool: Tool '${toolId}' does not have an execute function.`);
1397
- throw new Error(`Tool '${toolId}' cannot be executed.`);
1398
- }
1399
- } catch (error) {
1400
- const mastraError = new MastraError(
1401
- {
1402
- id: 'MCP_SERVER_TOOL_EXECUTE_PREPARATION_FAILED',
1403
- domain: ErrorDomain.MCP,
1404
- category: ErrorCategory.USER,
1405
- details: {
1406
- toolId,
1407
- args,
1408
- },
1409
- },
1410
- error,
1411
- );
1412
- this.logger.trackException(mastraError);
1413
- throw mastraError;
1414
- }
1415
-
1416
- try {
1417
- const finalExecutionContext = {
1418
- messages: executionContext?.messages || [],
1419
- toolCallId: executionContext?.toolCallId || randomUUID(),
1420
- };
1421
- const result = await tool.execute(validatedArgs, finalExecutionContext);
1422
- this.logger.info(`ExecuteTool: Tool '${toolId}' executed successfully.`);
1423
- return result;
1424
- } catch (error) {
1425
- const mastraError = new MastraError(
1426
- {
1427
- id: 'MCP_SERVER_TOOL_EXECUTE_FAILED',
1428
- domain: ErrorDomain.MCP,
1429
- category: ErrorCategory.USER,
1430
- details: {
1431
- toolId,
1432
- validatedArgs: validatedArgs,
1433
- },
1434
- },
1435
- error,
1436
- );
1437
- this.logger.trackException(mastraError);
1438
- this.logger.error(`ExecuteTool: Tool execution failed for '${toolId}':`, { error });
1439
- throw mastraError;
1440
- }
1441
- }
1442
- }