@mastra/mcp 0.10.0 → 0.10.1

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.
@@ -1,8 +1,9 @@
1
- import { randomUUID } from 'crypto';
1
+ import { randomUUID } from 'node:crypto';
2
2
  import type * as http from 'node:http';
3
3
  import type { InternalCoreTool } from '@mastra/core';
4
- import { makeCoreTool } from '@mastra/core';
4
+ import { createTool, makeCoreTool } from '@mastra/core';
5
5
  import type { ToolsInput } from '@mastra/core/agent';
6
+ import { Agent } from '@mastra/core/agent';
6
7
  import { MCPServerBase } from '@mastra/core/mcp';
7
8
  import type {
8
9
  MCPServerConfig,
@@ -13,17 +14,46 @@ import type {
13
14
  MCPServerSSEOptions,
14
15
  } from '@mastra/core/mcp';
15
16
  import { RuntimeContext } from '@mastra/core/runtime-context';
17
+ import type { Workflow } from '@mastra/core/workflows';
16
18
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
19
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
18
20
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
21
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
20
22
  import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
21
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
23
+ import {
24
+ CallToolRequestSchema,
25
+ ListToolsRequestSchema,
26
+ ListResourcesRequestSchema,
27
+ ReadResourceRequestSchema,
28
+ ListResourceTemplatesRequestSchema,
29
+ SubscribeRequestSchema,
30
+ UnsubscribeRequestSchema,
31
+ } from '@modelcontextprotocol/sdk/types.js';
32
+ import type {
33
+ ResourceContents,
34
+ Resource,
35
+ ResourceTemplate,
36
+ ServerCapabilities,
37
+ } from '@modelcontextprotocol/sdk/types.js';
22
38
  import type { SSEStreamingApi } from 'hono/streaming';
23
39
  import { streamSSE } from 'hono/streaming';
24
40
  import { SSETransport } from 'hono-mcp-server-sse-transport';
25
41
  import { z } from 'zod';
26
-
42
+ import { ServerResourceActions } from './resourceActions';
43
+
44
+ export type MCPServerResourceContentCallback = ({
45
+ uri,
46
+ }: {
47
+ uri: string;
48
+ }) => Promise<MCPServerResourceContent | MCPServerResourceContent[]>;
49
+ export type MCPServerResourceContent = { text?: string } | { blob?: string };
50
+ export type MCPServerResources = {
51
+ listResources: () => Promise<Resource[]>;
52
+ getResourceContent: MCPServerResourceContentCallback;
53
+ resourceTemplates?: () => Promise<ResourceTemplate[]>;
54
+ };
55
+
56
+ export type { Resource, ResourceTemplate };
27
57
  export class MCPServer extends MCPServerBase {
28
58
  private server: Server;
29
59
  private stdioTransport?: StdioServerTransport;
@@ -32,6 +62,17 @@ export class MCPServer extends MCPServerBase {
32
62
  private streamableHTTPTransport?: StreamableHTTPServerTransport;
33
63
  private listToolsHandlerIsRegistered: boolean = false;
34
64
  private callToolHandlerIsRegistered: boolean = false;
65
+ private listResourcesHandlerIsRegistered: boolean = false;
66
+ private readResourceHandlerIsRegistered: boolean = false;
67
+ private listResourceTemplatesHandlerIsRegistered: boolean = false;
68
+ private subscribeResourceHandlerIsRegistered: boolean = false;
69
+ private unsubscribeResourceHandlerIsRegistered: boolean = false;
70
+
71
+ private definedResources?: Resource[];
72
+ private definedResourceTemplates?: ResourceTemplate[];
73
+ private resourceOptions?: MCPServerResources;
74
+ private subscriptions: Set<string> = new Set();
75
+ public readonly resources: ServerResourceActions;
35
76
 
36
77
  /**
37
78
  * Get the current stdio transport.
@@ -65,30 +106,216 @@ export class MCPServer extends MCPServerBase {
65
106
  * Construct a new MCPServer instance.
66
107
  * @param opts - Configuration options for the server, including registry metadata.
67
108
  */
68
- constructor(opts: MCPServerConfig) {
109
+ constructor(opts: MCPServerConfig & { resources?: MCPServerResources }) {
69
110
  super(opts);
111
+ this.resourceOptions = opts.resources;
70
112
 
71
- this.server = new Server(
72
- { name: this.name, version: this.version },
73
- { capabilities: { tools: {}, logging: { enabled: true } } },
74
- );
113
+ const capabilities: ServerCapabilities = {
114
+ tools: {},
115
+ logging: { enabled: true },
116
+ };
117
+
118
+ if (opts.resources) {
119
+ capabilities.resources = { subscribe: true, listChanged: true };
120
+ }
121
+
122
+ this.server = new Server({ name: this.name, version: this.version }, { capabilities });
75
123
 
76
124
  this.logger.info(
77
- `Initialized MCPServer '${this.name}' v${this.version} (ID: ${this.id}) with tools: ${Object.keys(this.convertedTools).join(', ')}`,
125
+ `Initialized MCPServer '${this.name}' v${this.version} (ID: ${this.id}) with tools: ${Object.keys(this.convertedTools).join(', ')} and resources. Capabilities: ${JSON.stringify(capabilities)}`,
78
126
  );
79
127
 
80
128
  this.sseHonoTransports = new Map();
81
129
  this.registerListToolsHandler();
82
130
  this.registerCallToolHandler();
131
+ if (opts.resources) {
132
+ this.registerListResourcesHandler();
133
+ this.registerReadResourceHandler({ getResourcesCallback: opts.resources.getResourceContent });
134
+ this.registerSubscribeResourceHandler();
135
+ this.registerUnsubscribeResourceHandler();
136
+
137
+ if (opts.resources.resourceTemplates) {
138
+ this.registerListResourceTemplatesHandler();
139
+ }
140
+ }
141
+ this.resources = new ServerResourceActions({
142
+ getSubscriptions: () => this.subscriptions,
143
+ getLogger: () => this.logger,
144
+ getSdkServer: () => this.server,
145
+ clearDefinedResources: () => {
146
+ this.definedResources = undefined;
147
+ },
148
+ clearDefinedResourceTemplates: () => {
149
+ this.definedResourceTemplates = undefined;
150
+ },
151
+ });
152
+ }
153
+
154
+ private convertAgentsToTools(
155
+ agentsConfig?: Record<string, Agent>,
156
+ definedConvertedTools?: Record<string, ConvertedTool>,
157
+ ): Record<string, ConvertedTool> {
158
+ const agentTools: Record<string, ConvertedTool> = {};
159
+ if (!agentsConfig) {
160
+ return agentTools;
161
+ }
162
+
163
+ for (const agentKey in agentsConfig) {
164
+ const agent = agentsConfig[agentKey];
165
+ if (!agent || !(agent instanceof Agent)) {
166
+ this.logger.warn(`Agent instance for '${agentKey}' is invalid or missing a generate function. Skipping.`);
167
+ continue;
168
+ }
169
+
170
+ const agentDescription = agent.getDescription();
171
+
172
+ if (!agentDescription) {
173
+ throw new Error(
174
+ `Agent '${agent.name}' (key: '${agentKey}') must have a non-empty description to be used in an MCPServer.`,
175
+ );
176
+ }
177
+
178
+ const agentToolName = `ask_${agentKey}`;
179
+ if (definedConvertedTools?.[agentToolName] || agentTools[agentToolName]) {
180
+ this.logger.warn(
181
+ `Tool with name '${agentToolName}' already exists. Agent '${agentKey}' will not be added as a duplicate tool.`,
182
+ );
183
+ continue;
184
+ }
185
+
186
+ const agentToolDefinition = createTool({
187
+ id: agentToolName,
188
+ description: `Ask agent '${agent.name}' a question. Agent description: ${agentDescription}`,
189
+ inputSchema: z.object({
190
+ message: z.string().describe('The question or input for the agent.'),
191
+ }),
192
+ execute: async ({ context, runtimeContext }) => {
193
+ this.logger.debug(
194
+ `Executing agent tool '${agentToolName}' for agent '${agent.name}' with message: "${context.message}"`,
195
+ );
196
+ try {
197
+ const response = await agent.generate(context.message, { runtimeContext });
198
+ return response;
199
+ } catch (error) {
200
+ this.logger.error(`Error executing agent tool '${agentToolName}' for agent '${agent.name}':`, error);
201
+ throw error;
202
+ }
203
+ },
204
+ });
205
+
206
+ const options = {
207
+ name: agentToolName,
208
+ logger: this.logger,
209
+ mastra: this.mastra,
210
+ runtimeContext: new RuntimeContext(),
211
+ description: agentToolDefinition.description,
212
+ };
213
+ const coreTool = makeCoreTool(agentToolDefinition, options) as InternalCoreTool;
214
+
215
+ agentTools[agentToolName] = {
216
+ name: agentToolName,
217
+ description: coreTool.description,
218
+ parameters: coreTool.parameters,
219
+ execute: coreTool.execute!,
220
+ };
221
+ this.logger.info(`Registered agent '${agent.name}' (key: '${agentKey}') as tool: '${agentToolName}'`);
222
+ }
223
+ return agentTools;
224
+ }
225
+
226
+ private convertWorkflowsToTools(
227
+ workflowsConfig?: Record<string, Workflow>,
228
+ definedConvertedTools?: Record<string, ConvertedTool>,
229
+ ): Record<string, ConvertedTool> {
230
+ const workflowTools: Record<string, ConvertedTool> = {};
231
+ if (!workflowsConfig) {
232
+ return workflowTools;
233
+ }
234
+
235
+ for (const workflowKey in workflowsConfig) {
236
+ const workflow = workflowsConfig[workflowKey];
237
+ if (!workflow || typeof workflow.createRun !== 'function') {
238
+ this.logger.warn(
239
+ `Workflow instance for '${workflowKey}' is invalid or missing a createRun function. Skipping.`,
240
+ );
241
+ continue;
242
+ }
243
+
244
+ const workflowDescription = workflow.description;
245
+ if (!workflowDescription) {
246
+ throw new Error(
247
+ `Workflow '${workflow.id}' (key: '${workflowKey}') must have a non-empty description to be used in an MCPServer.`,
248
+ );
249
+ }
250
+
251
+ const workflowToolName = `run_${workflowKey}`;
252
+ if (definedConvertedTools?.[workflowToolName] || workflowTools[workflowToolName]) {
253
+ this.logger.warn(
254
+ `Tool with name '${workflowToolName}' already exists. Workflow '${workflowKey}' will not be added as a duplicate tool.`,
255
+ );
256
+ continue;
257
+ }
258
+
259
+ const workflowToolDefinition = createTool({
260
+ id: workflowToolName,
261
+ description: `Run workflow '${workflowKey}'. Workflow description: ${workflowDescription}`,
262
+ inputSchema: workflow.inputSchema,
263
+ execute: async ({ context, runtimeContext }) => {
264
+ this.logger.debug(
265
+ `Executing workflow tool '${workflowToolName}' for workflow '${workflow.id}' with input:`,
266
+ context,
267
+ );
268
+ try {
269
+ const run = workflow.createRun({ runId: runtimeContext?.get('runId') });
270
+
271
+ const response = await run.start({ inputData: context, runtimeContext });
272
+
273
+ return response;
274
+ } catch (error) {
275
+ this.logger.error(
276
+ `Error executing workflow tool '${workflowToolName}' for workflow '${workflow.id}':`,
277
+ error,
278
+ );
279
+ throw error;
280
+ }
281
+ },
282
+ });
283
+
284
+ const options = {
285
+ name: workflowToolName,
286
+ logger: this.logger,
287
+ mastra: this.mastra,
288
+ runtimeContext: new RuntimeContext(),
289
+ description: workflowToolDefinition.description,
290
+ };
291
+ const coreTool = makeCoreTool(workflowToolDefinition, options) as InternalCoreTool;
292
+
293
+ workflowTools[workflowToolName] = {
294
+ name: workflowToolName,
295
+ description: coreTool.description,
296
+ parameters: coreTool.parameters,
297
+ execute: coreTool.execute!,
298
+ };
299
+ this.logger.info(`Registered workflow '${workflow.id}' (key: '${workflowKey}') as tool: '${workflowToolName}'`);
300
+ }
301
+ return workflowTools;
83
302
  }
84
303
 
85
304
  /**
86
305
  * Convert and validate all provided tools, logging registration status.
306
+ * Also converts agents and workflows into tools.
87
307
  * @param tools Tool definitions
308
+ * @param agentsConfig Agent definitions to be converted to tools, expected from MCPServerConfig
309
+ * @param workflowsConfig Workflow definitions to be converted to tools, expected from MCPServerConfig
88
310
  * @returns Converted tools registry
89
311
  */
90
- convertTools(tools: ToolsInput): Record<string, ConvertedTool> {
91
- const convertedTools: Record<string, ConvertedTool> = {};
312
+ convertTools(
313
+ tools: ToolsInput,
314
+ agentsConfig?: Record<string, Agent>,
315
+ workflowsConfig?: Record<string, Workflow>,
316
+ ): Record<string, ConvertedTool> {
317
+ const definedConvertedTools: Record<string, ConvertedTool> = {};
318
+
92
319
  for (const toolName of Object.keys(tools)) {
93
320
  const toolInstance = tools[toolName];
94
321
  if (!toolInstance) {
@@ -111,16 +338,30 @@ export class MCPServer extends MCPServerBase {
111
338
 
112
339
  const coreTool = makeCoreTool(toolInstance, options) as InternalCoreTool;
113
340
 
114
- convertedTools[toolName] = {
341
+ definedConvertedTools[toolName] = {
115
342
  name: toolName,
116
343
  description: coreTool.description,
117
344
  parameters: coreTool.parameters,
118
345
  execute: coreTool.execute!,
119
346
  };
120
- this.logger.info(`Registered tool: '${toolName}' [${toolInstance?.description || 'No description'}]`);
347
+ this.logger.info(`Registered explicit tool: '${toolName}'`);
121
348
  }
122
- this.logger.info(`Total tools registered: ${Object.keys(convertedTools).length}`);
123
- return convertedTools;
349
+ this.logger.info(`Total defined tools registered: ${Object.keys(definedConvertedTools).length}`);
350
+
351
+ const agentDerivedTools = this.convertAgentsToTools(agentsConfig, definedConvertedTools);
352
+ const workflowDerivedTools = this.convertWorkflowsToTools(workflowsConfig, definedConvertedTools);
353
+
354
+ const allConvertedTools = { ...definedConvertedTools, ...agentDerivedTools, ...workflowDerivedTools };
355
+
356
+ const finalToolCount = Object.keys(allConvertedTools).length;
357
+ const definedCount = Object.keys(definedConvertedTools).length;
358
+ const fromAgentsCount = Object.keys(agentDerivedTools).length;
359
+ const fromWorkflowsCount = Object.keys(workflowDerivedTools).length;
360
+ this.logger.info(
361
+ `${finalToolCount} total tools registered (${definedCount} defined + ${fromAgentsCount} agents + ${fromWorkflowsCount} workflows)`,
362
+ );
363
+
364
+ return allConvertedTools;
124
365
  }
125
366
 
126
367
  /**
@@ -222,6 +463,187 @@ export class MCPServer extends MCPServerBase {
222
463
  });
223
464
  }
224
465
 
466
+ /**
467
+ * Register the ListResources handler for listing all available resources.
468
+ */
469
+ private registerListResourcesHandler() {
470
+ if (this.listResourcesHandlerIsRegistered) {
471
+ return;
472
+ }
473
+ this.listResourcesHandlerIsRegistered = true;
474
+ const capturedResourceOptions = this.resourceOptions; // Capture for TS narrowing
475
+
476
+ if (!capturedResourceOptions?.listResources) {
477
+ this.logger.warn('ListResources capability not supported by server configuration.');
478
+ return;
479
+ }
480
+
481
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
482
+ this.logger.debug('Handling ListResources request');
483
+ if (this.definedResources) {
484
+ return { resources: this.definedResources };
485
+ } else {
486
+ try {
487
+ const resources = await capturedResourceOptions.listResources();
488
+ // Cache the resources
489
+ this.definedResources = resources;
490
+ this.logger.debug(`Fetched and cached ${this.definedResources.length} resources.`);
491
+ return { resources: this.definedResources };
492
+ } catch (error) {
493
+ this.logger.error('Error fetching resources via listResources():', { error });
494
+ // Re-throw to let the MCP Server SDK handle formatting the error response
495
+ throw error;
496
+ }
497
+ }
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Register the ReadResource handler for reading a resource by URI.
503
+ */
504
+ private registerReadResourceHandler({
505
+ getResourcesCallback,
506
+ }: {
507
+ getResourcesCallback: MCPServerResourceContentCallback;
508
+ }) {
509
+ if (this.readResourceHandlerIsRegistered) {
510
+ return;
511
+ }
512
+ this.readResourceHandlerIsRegistered = true;
513
+ this.server.setRequestHandler(ReadResourceRequestSchema, async request => {
514
+ const startTime = Date.now();
515
+ const uri = request.params.uri;
516
+ this.logger.debug(`Handling ReadResource request for URI: ${uri}`);
517
+
518
+ if (!this.definedResources) {
519
+ const resources = await this.resourceOptions?.listResources?.();
520
+ if (!resources) throw new Error('Failed to load resources');
521
+ this.definedResources = resources;
522
+ }
523
+
524
+ const resource = this.definedResources?.find(r => r.uri === uri);
525
+
526
+ if (!resource) {
527
+ this.logger.warn(`ReadResource: Unknown resource URI '${uri}' requested.`);
528
+ throw new Error(`Resource not found: ${uri}`);
529
+ }
530
+
531
+ try {
532
+ const resourcesOrResourceContent = await getResourcesCallback({ uri });
533
+ const resourcesContent = Array.isArray(resourcesOrResourceContent)
534
+ ? resourcesOrResourceContent
535
+ : [resourcesOrResourceContent];
536
+ const contents: ResourceContents[] = resourcesContent.map(resourceContent => {
537
+ const contentItem: ResourceContents = {
538
+ uri: resource.uri,
539
+ mimeType: resource.mimeType,
540
+ };
541
+ if ('text' in resourceContent) {
542
+ contentItem.text = resourceContent.text;
543
+ }
544
+
545
+ if ('blob' in resourceContent) {
546
+ contentItem.blob = resourceContent.blob;
547
+ }
548
+
549
+ return contentItem;
550
+ });
551
+ const duration = Date.now() - startTime;
552
+ this.logger.info(`Resource '${uri}' read successfully in ${duration}ms.`);
553
+ return {
554
+ contents,
555
+ };
556
+ } catch (error) {
557
+ const duration = Date.now() - startTime;
558
+ this.logger.error(`Failed to get content for resource URI '${uri}' in ${duration}ms`, { error });
559
+ throw error;
560
+ }
561
+ });
562
+ }
563
+
564
+ /**
565
+ * Register the ListResourceTemplates handler.
566
+ */
567
+ private registerListResourceTemplatesHandler() {
568
+ if (this.listResourceTemplatesHandlerIsRegistered) {
569
+ return;
570
+ }
571
+
572
+ // If this method is called, this.resourceOptions and this.resourceOptions.resourceTemplates should exist
573
+ // due to the constructor logic checking opts.resources.resourceTemplates.
574
+ if (!this.resourceOptions || typeof this.resourceOptions.resourceTemplates !== 'function') {
575
+ this.logger.warn(
576
+ 'ListResourceTemplates handler called, but resourceTemplates function is not available on resourceOptions or not a function.',
577
+ );
578
+ // Register a handler that returns empty templates if not properly configured.
579
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
580
+ this.logger.debug('Handling ListResourceTemplates request (no templates configured or resourceOptions issue)');
581
+ return { resourceTemplates: [] };
582
+ });
583
+ this.listResourceTemplatesHandlerIsRegistered = true;
584
+ return;
585
+ }
586
+
587
+ // Typescript can now infer resourceTemplatesFn is a function.
588
+ const resourceTemplatesFn = this.resourceOptions.resourceTemplates;
589
+
590
+ this.listResourceTemplatesHandlerIsRegistered = true;
591
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
592
+ this.logger.debug('Handling ListResourceTemplates request');
593
+ if (this.definedResourceTemplates) {
594
+ return { resourceTemplates: this.definedResourceTemplates };
595
+ } else {
596
+ try {
597
+ const templates = await resourceTemplatesFn(); // Safe to call now
598
+ this.definedResourceTemplates = templates;
599
+ this.logger.debug(`Fetched and cached ${this.definedResourceTemplates.length} resource templates.`);
600
+ return { resourceTemplates: this.definedResourceTemplates };
601
+ } catch (error) {
602
+ this.logger.error('Error fetching resource templates via resourceTemplates():', { error });
603
+ // Re-throw to let the MCP Server SDK handle formatting the error response
604
+ throw error;
605
+ }
606
+ }
607
+ });
608
+ }
609
+
610
+ /**
611
+ * Register the SubscribeResource handler.
612
+ */
613
+ private registerSubscribeResourceHandler() {
614
+ if (this.subscribeResourceHandlerIsRegistered) {
615
+ return;
616
+ }
617
+ if (!SubscribeRequestSchema) {
618
+ this.logger.warn('SubscribeRequestSchema not available, cannot register SubscribeResource handler.');
619
+ return;
620
+ }
621
+ this.subscribeResourceHandlerIsRegistered = true;
622
+ this.server.setRequestHandler(SubscribeRequestSchema as any, async (request: { params: { uri: string } }) => {
623
+ const uri = request.params.uri;
624
+ this.logger.info(`Received resources/subscribe request for URI: ${uri}`);
625
+ this.subscriptions.add(uri);
626
+ return {};
627
+ });
628
+ }
629
+
630
+ /**
631
+ * Register the UnsubscribeResource handler.
632
+ */
633
+ private registerUnsubscribeResourceHandler() {
634
+ if (this.unsubscribeResourceHandlerIsRegistered) {
635
+ return;
636
+ }
637
+ this.unsubscribeResourceHandlerIsRegistered = true;
638
+
639
+ this.server.setRequestHandler(UnsubscribeRequestSchema as any, async (request: { params: { uri: string } }) => {
640
+ const uri = request.params.uri;
641
+ this.logger.info(`Received resources/unsubscribe request for URI: ${uri}`);
642
+ this.subscriptions.delete(uri);
643
+ return {};
644
+ });
645
+ }
646
+
225
647
  /**
226
648
  * Start the MCP server using stdio transport (for Windsurf integration).
227
649
  */
@@ -413,6 +835,12 @@ export class MCPServer extends MCPServerBase {
413
835
  async close() {
414
836
  this.callToolHandlerIsRegistered = false;
415
837
  this.listToolsHandlerIsRegistered = false;
838
+ this.listResourcesHandlerIsRegistered = false;
839
+ this.readResourceHandlerIsRegistered = false;
840
+ this.listResourceTemplatesHandlerIsRegistered = false;
841
+ this.subscribeResourceHandlerIsRegistered = false;
842
+ this.unsubscribeResourceHandlerIsRegistered = false;
843
+
416
844
  try {
417
845
  if (this.stdioTransport) {
418
846
  await this.stdioTransport.close?.();