@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.
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +7 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/package.json +19 -6
- package/.turbo/turbo-build.log +0 -4
- package/eslint.config.js +0 -11
- package/integration-tests/node_modules/.bin/tsc +0 -21
- package/integration-tests/node_modules/.bin/tsserver +0 -21
- package/integration-tests/node_modules/.bin/vitest +0 -21
- package/integration-tests/package.json +0 -29
- package/integration-tests/src/mastra/agents/weather.ts +0 -34
- package/integration-tests/src/mastra/index.ts +0 -15
- package/integration-tests/src/mastra/mcp/index.ts +0 -46
- package/integration-tests/src/mastra/tools/weather.ts +0 -13
- package/integration-tests/src/server.test.ts +0 -238
- package/integration-tests/tsconfig.json +0 -13
- package/integration-tests/vitest.config.ts +0 -14
- package/src/__fixtures__/fire-crawl-complex-schema.ts +0 -1013
- package/src/__fixtures__/server-weather.ts +0 -16
- package/src/__fixtures__/stock-price.ts +0 -128
- package/src/__fixtures__/tools.ts +0 -94
- package/src/__fixtures__/weather.ts +0 -269
- package/src/client/client.test.ts +0 -585
- package/src/client/client.ts +0 -628
- package/src/client/configuration.test.ts +0 -856
- package/src/client/configuration.ts +0 -468
- package/src/client/elicitationActions.ts +0 -26
- package/src/client/index.ts +0 -3
- package/src/client/promptActions.ts +0 -70
- package/src/client/resourceActions.ts +0 -119
- package/src/index.ts +0 -2
- package/src/server/index.ts +0 -2
- package/src/server/promptActions.ts +0 -48
- package/src/server/resourceActions.ts +0 -90
- package/src/server/server-logging.test.ts +0 -181
- package/src/server/server.test.ts +0 -2142
- package/src/server/server.ts +0 -1442
- package/src/server/types.ts +0 -59
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -17
- package/vitest.config.ts +0 -8
package/src/server/server.ts
DELETED
|
@@ -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
|
-
}
|