@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,6 +37,7 @@
37
37
  "devDependencies": {
38
38
  "@ai-sdk/anthropic": "^1.1.15",
39
39
  "@ai-sdk/openai": "^1.3.22",
40
+ "ai": "4.3.16",
40
41
  "@hono/node-server": "^1.13.8",
41
42
  "@mendable/firecrawl-js": "^1.24.0",
42
43
  "@microsoft/api-extractor": "^7.52.5",
@@ -49,8 +50,8 @@
49
50
  "vitest": "^3.1.2",
50
51
  "zod": "^3.24.3",
51
52
  "zod-to-json-schema": "^3.24.5",
52
- "@internal/lint": "0.0.6",
53
- "@mastra/core": "0.10.0"
53
+ "@internal/lint": "0.0.7",
54
+ "@mastra/core": "0.10.1"
54
55
  },
55
56
  "scripts": {
56
57
  "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
@@ -4,7 +4,7 @@ import type { ToolsInput } from '@mastra/core/agent';
4
4
  import FirecrawlApp from '@mendable/firecrawl-js';
5
5
  import type { ScrapeParams, MapParams, CrawlParams, FirecrawlDocument } from '@mendable/firecrawl-js';
6
6
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
7
- import { MCPServer } from '../server';
7
+ import { MCPServer } from '../server/server';
8
8
 
9
9
  // Tool definitions
10
10
  const SCRAPE_TOOL: Tool = {
@@ -1,4 +1,4 @@
1
- import { MCPServer } from '../server';
1
+ import { MCPServer } from '../server/server';
2
2
  import { weatherTool } from './tools';
3
3
 
4
4
  const server = new MCPServer({
@@ -1,15 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'http';
2
2
  import { createServer } from 'http';
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- ListResourcesRequestSchema,
9
- ReadResourceRequestSchema,
10
- } from '@modelcontextprotocol/sdk/types.js';
3
+ import { createTool } from '@mastra/core';
4
+ import type { Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
11
5
  import { z } from 'zod';
12
- import { zodToJsonSchema } from 'zod-to-json-schema';
6
+ import { MCPServer } from '../server/server';
7
+ import type { MCPServerResources, MCPServerResourceContent } from '../server/server';
13
8
 
14
9
  const getWeather = async (location: string) => {
15
10
  // Return mock data for testing
@@ -24,29 +19,20 @@ const getWeather = async (location: string) => {
24
19
  };
25
20
  };
26
21
 
27
- const server = new Server(
28
- {
29
- name: 'Weather Server',
30
- version: '1.0.0',
31
- },
32
- {
33
- capabilities: {
34
- tools: {},
35
- resources: {},
36
- },
37
- },
38
- );
22
+ const serverId = 'weather-server-fixture';
23
+ console.log(`[${serverId}] Initializing`);
39
24
 
40
25
  const weatherInputSchema = z.object({
41
26
  location: z.string().describe('City name'),
42
27
  });
43
28
 
44
- const weatherTool = {
45
- name: 'getWeather',
29
+ const weatherToolDefinition = createTool({
30
+ id: 'getWeather',
46
31
  description: 'Get current weather for a location',
47
- execute: async (args: z.infer<typeof weatherInputSchema>) => {
32
+ inputSchema: weatherInputSchema,
33
+ execute: async ({ context }) => {
48
34
  try {
49
- const weatherData = await getWeather(args.location);
35
+ const weatherData = await getWeather(context.location);
50
36
  return {
51
37
  content: [
52
38
  {
@@ -57,43 +43,21 @@ const weatherTool = {
57
43
  isError: false,
58
44
  };
59
45
  } catch (error) {
60
- if (error instanceof Error) {
61
- return {
62
- content: [
63
- {
64
- type: 'text',
65
- text: `Weather fetch failed: ${error.message}`,
66
- },
67
- ],
68
- isError: true,
69
- };
70
- }
46
+ const message = error instanceof Error ? error.message : String(error);
71
47
  return {
72
48
  content: [
73
49
  {
74
50
  type: 'text',
75
- text: 'An unknown error occurred.',
51
+ text: `Weather fetch failed: ${message}`,
76
52
  },
77
53
  ],
78
54
  isError: true,
79
55
  };
80
56
  }
81
57
  },
82
- };
83
-
84
- // Set up request handlers
85
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
86
- tools: [
87
- {
88
- name: weatherTool.name,
89
- description: weatherTool.description,
90
- inputSchema: zodToJsonSchema(weatherInputSchema),
91
- },
92
- ],
93
- }));
58
+ });
94
59
 
95
- // Resources implementation
96
- const weatherResources = [
60
+ const weatherResourceDefinitions: Resource[] = [
97
61
  {
98
62
  uri: 'weather://current',
99
63
  name: 'Current Weather Data',
@@ -114,164 +78,132 @@ const weatherResources = [
114
78
  },
115
79
  ];
116
80
 
117
- // List available resources
118
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
119
- resources: weatherResources,
120
- }));
121
-
122
- // Read resource contents
123
- server.setRequestHandler(ReadResourceRequestSchema, async request => {
124
- const uri = request.params.uri;
125
-
126
- if (uri === 'weather://current') {
127
- return {
128
- contents: [
129
- {
130
- uri,
131
- mimeType: 'application/json',
132
- text: JSON.stringify({
133
- location: 'San Francisco',
134
- temperature: 18,
135
- conditions: 'Partly Cloudy',
136
- humidity: 65,
137
- windSpeed: 12,
138
- updated: new Date().toISOString(),
139
- }),
140
- },
141
- ],
142
- };
143
- } else if (uri === 'weather://forecast') {
144
- return {
145
- contents: [
146
- {
147
- uri,
148
- mimeType: 'application/json',
149
- text: JSON.stringify([
150
- { day: 1, high: 19, low: 12, conditions: 'Sunny' },
151
- { day: 2, high: 22, low: 14, conditions: 'Clear' },
152
- { day: 3, high: 20, low: 13, conditions: 'Partly Cloudy' },
153
- { day: 4, high: 18, low: 11, conditions: 'Rain' },
154
- { day: 5, high: 17, low: 10, conditions: 'Showers' },
155
- ]),
156
- },
157
- ],
158
- };
159
- } else if (uri === 'weather://historical') {
160
- return {
161
- contents: [
162
- {
163
- uri,
164
- mimeType: 'application/json',
165
- text: JSON.stringify({
166
- averageHigh: 20,
167
- averageLow: 12,
168
- rainDays: 8,
169
- sunnyDays: 18,
170
- recordHigh: 28,
171
- recordLow: 7,
172
- }),
173
- },
174
- ],
175
- };
176
- }
81
+ const weatherResourceTemplatesDefinitions: ResourceTemplate[] = [
82
+ {
83
+ uriTemplate: 'weather://custom/{city}/{days}',
84
+ name: 'Custom Weather Forecast',
85
+ description: 'Generates a custom weather forecast for a city and number of days.',
86
+ mimeType: 'application/json',
87
+ },
88
+ ];
177
89
 
178
- throw new Error(`Resource not found: ${uri}`);
179
- });
90
+ const weatherResourceContents: Record<string, MCPServerResourceContent> = {
91
+ 'weather://current': {
92
+ text: JSON.stringify({
93
+ location: 'San Francisco',
94
+ temperature: 18,
95
+ conditions: 'Partly Cloudy',
96
+ humidity: 65,
97
+ windSpeed: 12,
98
+ updated: new Date().toISOString(),
99
+ }),
100
+ },
101
+ 'weather://forecast': {
102
+ text: JSON.stringify([
103
+ { day: 1, high: 19, low: 12, conditions: 'Sunny' },
104
+ { day: 2, high: 22, low: 14, conditions: 'Clear' },
105
+ { day: 3, high: 20, low: 13, conditions: 'Partly Cloudy' },
106
+ { day: 4, high: 18, low: 11, conditions: 'Rain' },
107
+ { day: 5, high: 17, low: 10, conditions: 'Showers' },
108
+ ]),
109
+ },
110
+ 'weather://historical': {
111
+ text: JSON.stringify({
112
+ averageHigh: 20,
113
+ averageLow: 12,
114
+ rainDays: 8,
115
+ sunnyDays: 18,
116
+ recordHigh: 28,
117
+ recordLow: 7,
118
+ }),
119
+ },
120
+ };
180
121
 
181
- server.setRequestHandler(CallToolRequestSchema, async request => {
182
- try {
183
- switch (request.params.name) {
184
- case 'getWeather': {
185
- const args = weatherInputSchema.parse(request.params.arguments);
186
- return await weatherTool.execute(args);
187
- }
188
- default:
189
- return {
190
- content: [
191
- {
192
- type: 'text',
193
- text: `Unknown tool: ${request.params.name}`,
194
- },
195
- ],
196
- isError: true,
197
- };
122
+ const mcpServerResources: MCPServerResources = {
123
+ listResources: async () => weatherResourceDefinitions,
124
+ getResourceContent: async ({ uri }: { uri: string }) => {
125
+ if (weatherResourceContents[uri]) {
126
+ return weatherResourceContents[uri];
198
127
  }
199
- } catch (error) {
200
- if (error instanceof z.ZodError) {
201
- return {
202
- content: [
203
- {
204
- type: 'text',
205
- text: `Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
206
- },
207
- ],
208
- isError: true,
209
- };
210
- }
211
- return {
212
- content: [
213
- {
214
- type: 'text',
215
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
216
- },
217
- ],
218
- isError: true,
219
- };
220
- }
221
- });
128
+ throw new Error(`Mock resource content not found for ${uri}`);
129
+ },
130
+ resourceTemplates: async () => weatherResourceTemplatesDefinitions,
131
+ };
222
132
 
223
- // Start the server
224
- let transport: SSEServerTransport | undefined;
133
+ const mcpServer = new MCPServer({
134
+ name: serverId,
135
+ version: '1.0.0',
136
+ tools: {
137
+ getWeather: weatherToolDefinition,
138
+ },
139
+ resources: mcpServerResources,
140
+ });
225
141
 
226
142
  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
227
143
  const url = new URL(req.url || '', `http://${req.headers.host}`);
144
+ const connectionLogPrefix = `[${serverId}] REQ: ${req.method} ${url.pathname}`;
145
+ console.log(connectionLogPrefix);
146
+
147
+ await mcpServer.startSSE({
148
+ url,
149
+ ssePath: '/sse',
150
+ messagePath: '/message',
151
+ req,
152
+ res,
153
+ });
154
+ });
228
155
 
229
- if (url.pathname === '/sse') {
230
- console.log('Received SSE connection');
231
- transport = new SSEServerTransport('/message', res);
232
- await server.connect(transport);
156
+ const PORT = process.env.WEATHER_SERVER_PORT || 60808;
157
+ console.log(`[${serverId}] Starting HTTP server on port ${PORT}`);
158
+ httpServer.listen(PORT, () => {
159
+ console.log(`[${serverId}] Weather server is running on SSE at http://localhost:${PORT}`);
160
+ });
233
161
 
234
- server.onclose = async () => {
235
- await server.close();
236
- transport = undefined;
237
- };
162
+ // --- Interval-based Notifications ---
163
+ const NOTIFICATION_INTERVAL_MS = 1500;
164
+ let resourceUpdateCounter = 0;
165
+
166
+ const notificationInterval = setInterval(async () => {
167
+ // Simulate resource update for weather://current
168
+ resourceUpdateCounter++;
169
+ const newCurrentWeatherText = JSON.stringify({
170
+ location: 'San Francisco',
171
+ temperature: 18 + (resourceUpdateCounter % 5), // Vary temperature slightly
172
+ conditions: resourceUpdateCounter % 2 === 0 ? 'Sunny' : 'Partly Cloudy',
173
+ humidity: 65 + (resourceUpdateCounter % 3),
174
+ windSpeed: 12 + (resourceUpdateCounter % 4),
175
+ updated: new Date().toISOString(),
176
+ });
177
+ weatherResourceContents['weather://current'] = { text: newCurrentWeatherText };
238
178
 
239
- // Handle client disconnection
240
- res.on('close', () => {
241
- transport = undefined;
242
- });
243
- } else if (url.pathname === '/message') {
244
- console.log('Received message');
245
- if (!transport) {
246
- res.writeHead(503);
247
- res.end('SSE connection not established');
248
- return;
249
- }
250
- await transport.handlePostMessage(req, res);
251
- } else {
252
- console.log('Unknown path:', url.pathname);
253
- res.writeHead(404);
254
- res.end();
179
+ const updatePrefix = `[${serverId}] IntervalUpdate`;
180
+ try {
181
+ await mcpServer.resources.notifyUpdated({ uri: 'weather://current' });
182
+ } catch (e: any) {
183
+ console.error(`${updatePrefix} - Error sending resourceUpdated for weather://current via MCPServer: ${e.message}`);
255
184
  }
256
- });
257
185
 
258
- const PORT = process.env.PORT || 60808;
259
- httpServer.listen(PORT, () => {
260
- console.log(`Weather server is running on SSE at http://localhost:${PORT}`);
261
- });
186
+ // Simulate resource list changed (less frequently, e.g., every 3rd interval)
187
+ if (resourceUpdateCounter % 3 === 0) {
188
+ const listChangePrefix = `[${serverId}] IntervalListChange`;
189
+ try {
190
+ await mcpServer.resources.notifyListChanged();
191
+ } catch (e: any) {
192
+ console.error(`${listChangePrefix} - Error sending resourceListChanged via MCPServer: ${e.message}`);
193
+ }
194
+ }
195
+ }, NOTIFICATION_INTERVAL_MS);
196
+ // --- End Interval-based Notifications ---
262
197
 
263
198
  // Handle graceful shutdown
264
199
  process.on('SIGINT', async () => {
265
200
  console.log('Shutting down weather server...');
266
- if (transport) {
267
- await server.close();
268
- transport = undefined;
269
- }
270
- // Close the HTTP server
201
+ clearInterval(notificationInterval); // Clear the interval
202
+ await mcpServer.close();
271
203
  httpServer.close(() => {
272
204
  console.log('Weather server shut down complete');
273
205
  process.exit(0);
274
206
  });
275
207
  });
276
208
 
277
- export { server };
209
+ export { mcpServer as server };
@@ -18,6 +18,7 @@ async function setupTestServer(withSessionManagement: boolean) {
18
18
  capabilities: {
19
19
  logging: {},
20
20
  tools: {},
21
+ resources: {},
21
22
  },
22
23
  },
23
24
  );
@@ -35,6 +36,17 @@ async function setupTestServer(withSessionManagement: boolean) {
35
36
  },
36
37
  );
37
38
 
39
+ mcpServer.resource('test-resource', 'resource://test', () => {
40
+ return {
41
+ contents: [
42
+ {
43
+ uri: 'resource://test',
44
+ text: 'Hello, world!',
45
+ },
46
+ ],
47
+ };
48
+ });
49
+
38
50
  const serverTransport = new StreamableHTTPServerTransport({
39
51
  sessionIdGenerator: withSessionManagement ? () => randomUUID() : undefined,
40
52
  });
@@ -94,6 +106,21 @@ describe('MastraMCPClient with Streamable HTTP', () => {
94
106
  const result = await tools.greet.execute({ context: { name: 'Stateless' } });
95
107
  expect(result).toEqual({ content: [{ type: 'text', text: 'Hello, Stateless!' }] });
96
108
  });
109
+
110
+ it('should list resources', async () => {
111
+ const resourcesResult = await client.listResources();
112
+ const resources = resourcesResult.resources;
113
+ expect(resources).toBeInstanceOf(Array);
114
+ const testResource = resources.find((r) => r.uri === 'resource://test');
115
+ expect(testResource).toBeDefined();
116
+ expect(testResource!.name).toBe('test-resource');
117
+ expect(testResource!.uri).toBe('resource://test');
118
+
119
+ const readResult = await client.readResource('resource://test');
120
+ expect(readResult.contents).toBeInstanceOf(Array);
121
+ expect(readResult.contents.length).toBe(1);
122
+ expect(readResult.contents[0].text).toBe('Hello, world!');
123
+ });
97
124
  });
98
125
 
99
126
  describe('Stateful Mode', () => {
@@ -10,14 +10,22 @@ import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotoc
10
10
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
11
11
  import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
12
12
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
13
- import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
14
13
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
15
14
  import type { ClientCapabilities, LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
16
- import { CallToolResultSchema, ListResourcesResultSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import {
16
+ CallToolResultSchema,
17
+ ListResourcesResultSchema,
18
+ ReadResourceResultSchema,
19
+ ResourceListChangedNotificationSchema,
20
+ ResourceUpdatedNotificationSchema,
21
+ ListResourceTemplatesResultSchema,
22
+ } from '@modelcontextprotocol/sdk/types.js';
23
+
17
24
  import { asyncExitHook, gracefulExit } from 'exit-hook';
18
25
  import { z } from 'zod';
19
26
  import { convertJsonSchemaToZod } from 'zod-from-json-schema';
20
27
  import type { JSONSchema } from 'zod-from-json-schema';
28
+ import { ResourceClientActions } from './resourceActions';
21
29
 
22
30
  // Re-export MCP SDK LoggingLevel for convenience
23
31
  export type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
@@ -110,6 +118,7 @@ export class InternalMastraMCPClient extends MastraBase {
110
118
  private serverConfig: MastraMCPServerDefinition;
111
119
  private transport?: Transport;
112
120
  private currentOperationContext: RuntimeContext | null = null;
121
+ public readonly resources: ResourceClientActions;
113
122
 
114
123
  constructor({
115
124
  name,
@@ -137,6 +146,8 @@ export class InternalMastraMCPClient extends MastraBase {
137
146
 
138
147
  // Set up log message capturing
139
148
  this.setupLogging();
149
+
150
+ this.resources = new ResourceClientActions({ client: this, logger: this.logger });
140
151
  }
141
152
 
142
153
  /**
@@ -317,15 +328,57 @@ export class InternalMastraMCPClient extends MastraBase {
317
328
  }
318
329
  }
319
330
 
320
- // TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
321
-
322
- async resources(): Promise<ReturnType<Protocol<any, any, any>['request']>> {
331
+ async listResources() {
323
332
  this.log('debug', `Requesting resources from MCP server`);
324
333
  return await this.client.request({ method: 'resources/list' }, ListResourcesResultSchema, {
325
334
  timeout: this.timeout,
326
335
  });
327
336
  }
328
337
 
338
+ async readResource(uri: string) {
339
+ this.log('debug', `Reading resource from MCP server: ${uri}`);
340
+ return await this.client.request({ method: 'resources/read', params: { uri } }, ReadResourceResultSchema, {
341
+ timeout: this.timeout,
342
+ });
343
+ }
344
+
345
+ async subscribeResource(uri: string) {
346
+ this.log('debug', `Subscribing to resource on MCP server: ${uri}`);
347
+ return await this.client.request({ method: 'resources/subscribe', params: { uri } }, z.object({}), {
348
+ timeout: this.timeout,
349
+ });
350
+ }
351
+
352
+ async unsubscribeResource(uri: string) {
353
+ this.log('debug', `Unsubscribing from resource on MCP server: ${uri}`);
354
+ return await this.client.request({ method: 'resources/unsubscribe', params: { uri } }, z.object({}), {
355
+ timeout: this.timeout,
356
+ });
357
+ }
358
+
359
+ async listResourceTemplates() {
360
+ this.log('debug', `Requesting resource templates from MCP server`);
361
+ return await this.client.request({ method: 'resources/templates/list' }, ListResourceTemplatesResultSchema, {
362
+ timeout: this.timeout,
363
+ });
364
+ }
365
+
366
+ setResourceUpdatedNotificationHandler(
367
+ handler: (params: z.infer<typeof ResourceUpdatedNotificationSchema>['params']) => void,
368
+ ): void {
369
+ this.log('debug', 'Setting resource updated notification handler');
370
+ this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, notification => {
371
+ handler(notification.params);
372
+ });
373
+ }
374
+
375
+ setResourceListChangedNotificationHandler(handler: () => void): void {
376
+ this.log('debug', 'Setting resource list changed notification handler');
377
+ this.client.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
378
+ handler();
379
+ });
380
+ }
381
+
329
382
  private convertInputSchema(
330
383
  inputSchema: Awaited<ReturnType<Client['listTools']>>['tools'][0]['inputSchema'] | JSONSchema,
331
384
  ): z.ZodType {