@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.
@@ -3,8 +3,9 @@ import path from 'path';
3
3
  import { openai } from '@ai-sdk/openai';
4
4
  import { Agent } from '@mastra/core/agent';
5
5
  import { RuntimeContext } from '@mastra/core/di';
6
+ import type { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
6
7
  import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from 'vitest';
7
- import { allTools, mcpServerName } from './__fixtures__/fire-crawl-complex-schema';
8
+ import { allTools, mcpServerName } from '../__fixtures__/fire-crawl-complex-schema';
8
9
  import type { LogHandler, LogMessage } from './client';
9
10
  import { MCPClient } from './configuration';
10
11
 
@@ -14,10 +15,14 @@ describe('MCPClient', () => {
14
15
  let mcp: MCPClient;
15
16
  let weatherProcess: ReturnType<typeof spawn>;
16
17
  let clients: MCPClient[] = [];
18
+ let weatherServerPort: number;
17
19
 
18
20
  beforeAll(async () => {
21
+ weatherServerPort = 60000 + Math.floor(Math.random() * 1000); // Generate a random port
19
22
  // Start the weather SSE server
20
- weatherProcess = spawn('npx', ['-y', 'tsx', path.join(__dirname, '__fixtures__/weather.ts')]);
23
+ weatherProcess = spawn('npx', ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/weather.ts')], {
24
+ env: { ...process.env, WEATHER_SERVER_PORT: String(weatherServerPort) }, // Pass port as env var
25
+ });
21
26
 
22
27
  // Wait for SSE server to be ready
23
28
  let resolved = false;
@@ -42,17 +47,20 @@ describe('MCPClient', () => {
42
47
  });
43
48
 
44
49
  beforeEach(async () => {
50
+ // Give each MCPClient a unique ID to prevent re-initialization errors across tests
51
+ const testId = "testId"
45
52
  mcp = new MCPClient({
53
+ id: testId,
46
54
  servers: {
47
55
  stockPrice: {
48
56
  command: 'npx',
49
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
57
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
50
58
  env: {
51
59
  FAKE_CREDS: 'test',
52
60
  },
53
61
  },
54
62
  weather: {
55
- url: new URL('http://localhost:60808/sse'),
63
+ url: new URL(`http://localhost:${weatherServerPort}/sse`), // Use the dynamic port
56
64
  },
57
65
  },
58
66
  });
@@ -77,13 +85,13 @@ describe('MCPClient', () => {
77
85
  expect(mcp['serverConfigs']).toEqual({
78
86
  stockPrice: {
79
87
  command: 'npx',
80
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
88
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
81
89
  env: {
82
90
  FAKE_CREDS: 'test',
83
91
  },
84
92
  },
85
93
  weather: {
86
- url: new URL('http://localhost:60808/sse'),
94
+ url: new URL(`http://localhost:${weatherServerPort}/sse`),
87
95
  },
88
96
  });
89
97
  });
@@ -106,7 +114,7 @@ describe('MCPClient', () => {
106
114
  });
107
115
 
108
116
  it('should get resources from connected MCP servers', async () => {
109
- const resources = await mcp.getResources();
117
+ const resources = await mcp.resources.list();
110
118
 
111
119
  expect(resources).toHaveProperty('weather');
112
120
  expect(resources.weather).toBeDefined();
@@ -142,12 +150,116 @@ describe('MCPClient', () => {
142
150
  });
143
151
  });
144
152
 
153
+ it('should list resource templates from connected MCP servers', async () => {
154
+ const templates = await mcp.resources.templates();
155
+ expect(templates).toHaveProperty('weather');
156
+ expect(templates.weather).toBeDefined();
157
+ expect(templates.weather.length).toBeGreaterThan(0);
158
+ const customForecastTemplate = templates.weather.find(
159
+ (t: ResourceTemplate) => t.uriTemplate === 'weather://custom/{city}/{days}',
160
+ );
161
+ expect(customForecastTemplate).toBeDefined();
162
+ expect(customForecastTemplate).toMatchObject({
163
+ uriTemplate: 'weather://custom/{city}/{days}',
164
+ name: 'Custom Weather Forecast',
165
+ description: expect.any(String),
166
+ mimeType: 'application/json',
167
+ });
168
+ });
169
+
170
+ it('should read a specific resource from a server', async () => {
171
+ const resourceContent = await mcp.resources.read('weather', 'weather://current');
172
+ expect(resourceContent).toBeDefined();
173
+ expect(resourceContent.contents).toBeInstanceOf(Array);
174
+ expect(resourceContent.contents.length).toBe(1);
175
+ const contentItem = resourceContent.contents[0];
176
+ expect(contentItem.uri).toBe('weather://current');
177
+ expect(contentItem.mimeType).toBe('application/json');
178
+ expect(contentItem.text).toBeDefined();
179
+ let parsedText: any = {};
180
+ if (contentItem.text && typeof contentItem.text === 'string') {
181
+ try {
182
+ parsedText = JSON.parse(contentItem.text);
183
+ } catch {
184
+ // If parsing fails, parsedText remains an empty object
185
+ // console.error("Failed to parse resource content text:", _e);
186
+ }
187
+ }
188
+ expect(parsedText).toHaveProperty('location');
189
+ });
190
+
191
+ it('should subscribe and unsubscribe from a resource on a specific server', async () => {
192
+ const serverName = 'weather';
193
+ const resourceUri = 'weather://current';
194
+
195
+ const subResult = await mcp.resources.subscribe(serverName, resourceUri);
196
+ expect(subResult).toEqual({});
197
+
198
+ const unsubResult = await mcp.resources.unsubscribe(serverName, resourceUri);
199
+ expect(unsubResult).toEqual({});
200
+ });
201
+
202
+ it('should receive resource updated notification from a specific server', async () => {
203
+ const serverName = 'weather';
204
+ const resourceUri = 'weather://current';
205
+ let notificationReceived = false;
206
+ let receivedUri = '';
207
+
208
+ await mcp.resources.list(); // Initial call to establish connection if needed
209
+ // Create the promise for the notification BEFORE subscribing
210
+ const resourceUpdatedPromise = new Promise<void>((resolve, reject) => {
211
+ mcp.resources.onUpdated(serverName, (params: { uri: string }) => {
212
+ if (params.uri === resourceUri) {
213
+ notificationReceived = true;
214
+ receivedUri = params.uri;
215
+ resolve();
216
+ } else {
217
+ console.log(`[Test LOG] Received update for ${params.uri}, waiting for ${resourceUri}`);
218
+ }
219
+ });
220
+ setTimeout(() => reject(new Error(`Timeout waiting for resourceUpdated notification for ${resourceUri}`)), 4500);
221
+ });
222
+
223
+ await mcp.resources.subscribe(serverName, resourceUri); // Ensure subscription is active
224
+
225
+ await expect(resourceUpdatedPromise).resolves.toBeUndefined(); // Wait for the notification
226
+
227
+ expect(notificationReceived).toBe(true);
228
+ expect(receivedUri).toBe(resourceUri);
229
+
230
+ await mcp.resources.unsubscribe(serverName, resourceUri); // Cleanup
231
+ }, 5000);
232
+
233
+ it('should receive resource list changed notification from a specific server', async () => {
234
+ const serverName = 'weather';
235
+ let notificationReceived = false;
236
+
237
+ await mcp.resources.list(); // Initial call to establish connection
238
+
239
+ const resourceListChangedPromise = new Promise<void>((resolve, reject) => {
240
+ mcp.resources.onListChanged(serverName, () => {
241
+ notificationReceived = true;
242
+ resolve();
243
+ });
244
+ setTimeout(() => reject(new Error('Timeout waiting for resourceListChanged notification')), 4500);
245
+ });
246
+
247
+ // In a real scenario, something would trigger the server to send this.
248
+ // For the test, we rely on the interval in weather.ts or a direct call if available.
249
+ // Adding a small delay or an explicit trigger if the fixture supported it would be more robust.
250
+ // For now, we assume the interval in weather.ts will eventually fire it.
251
+
252
+ await expect(resourceListChangedPromise).resolves.toBeUndefined(); // Wait for the notification
253
+
254
+ expect(notificationReceived).toBe(true);
255
+ });
256
+
145
257
  it('should handle errors when getting resources', async () => {
146
258
  const errorClient = new MCPClient({
147
259
  id: 'error-test-client',
148
260
  servers: {
149
261
  weather: {
150
- url: new URL('http://localhost:60808/sse'),
262
+ url: new URL(`http://localhost:${weatherServerPort}/sse`),
151
263
  },
152
264
  nonexistentServer: {
153
265
  command: 'nonexistent-command',
@@ -157,7 +269,7 @@ describe('MCPClient', () => {
157
269
  });
158
270
 
159
271
  try {
160
- const resources = await errorClient.getResources();
272
+ const resources = await errorClient.resources.list();
161
273
 
162
274
  expect(resources).toHaveProperty('weather');
163
275
  expect(resources.weather).toBeDefined();
@@ -190,7 +302,7 @@ describe('MCPClient', () => {
190
302
  servers: {
191
303
  stockPrice: {
192
304
  command: 'npx',
193
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
305
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
194
306
  env: {
195
307
  FAKE_CREDS: 'test',
196
308
  },
@@ -209,13 +321,13 @@ describe('MCPClient', () => {
209
321
  servers: {
210
322
  stockPrice: {
211
323
  command: 'npx',
212
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
324
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
213
325
  env: {
214
326
  FAKE_CREDS: 'test',
215
327
  },
216
328
  },
217
329
  weather: {
218
- url: new URL('http://localhost:60808/sse'),
330
+ url: new URL(`http://localhost:${weatherServerPort}/sse`),
219
331
  },
220
332
  },
221
333
  });
@@ -229,7 +341,7 @@ describe('MCPClient', () => {
229
341
  servers: {
230
342
  stockPrice: {
231
343
  command: 'npx',
232
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
344
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
233
345
  env: {
234
346
  FAKE_CREDS: 'test',
235
347
  },
@@ -243,7 +355,7 @@ describe('MCPClient', () => {
243
355
  servers: {
244
356
  stockPrice: {
245
357
  command: 'npx',
246
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
358
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
247
359
  env: {
248
360
  FAKE_CREDS: 'test',
249
361
  },
@@ -381,7 +493,7 @@ describe('MCPClient', () => {
381
493
  servers: {
382
494
  'firecrawl-mcp': {
383
495
  command: 'npx',
384
- args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/fire-crawl-complex-schema.ts')],
496
+ args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/fire-crawl-complex-schema.ts')],
385
497
  logger: mockLogHandler,
386
498
  },
387
499
  },
@@ -390,7 +502,7 @@ describe('MCPClient', () => {
390
502
 
391
503
  afterEach(async () => {
392
504
  mockLogHandler.mockClear();
393
- await complexClient?.disconnect().catch(() => {});
505
+ await complexClient?.disconnect().catch(() => { });
394
506
  });
395
507
 
396
508
  it('should process tools from firecrawl-mcp without crashing', async () => {
@@ -427,7 +539,7 @@ describe('MCPClient', () => {
427
539
  servers: {
428
540
  stockPrice: {
429
541
  command: 'npx',
430
- args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
542
+ args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
431
543
  env: { FAKE_CREDS: 'test' },
432
544
  logger: loggerFn,
433
545
  },
@@ -471,7 +583,7 @@ describe('MCPClient', () => {
471
583
  servers: {
472
584
  stockPriceServer: {
473
585
  command: 'npx',
474
- args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
586
+ args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
475
587
  env: { FAKE_CREDS: 'test' },
476
588
  logger: loggerFn,
477
589
  },
@@ -513,7 +625,7 @@ describe('MCPClient', () => {
513
625
  servers: {
514
626
  stockPriceServer: {
515
627
  command: 'npx',
516
- args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
628
+ args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
517
629
  env: { FAKE_CREDS: 'test' },
518
630
  logger: loggerFn,
519
631
  },
@@ -567,13 +679,13 @@ describe('MCPClient', () => {
567
679
  servers: {
568
680
  serverX: {
569
681
  command: 'npx',
570
- args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')], // Re-use fixture, tool name will differ by server
682
+ args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')], // Re-use fixture, tool name will differ by server
571
683
  logger: sharedLoggerFn,
572
684
  env: { FAKE_CREDS: 'serverX-creds' }, // Make env slightly different for clarity if needed
573
685
  },
574
686
  serverY: {
575
687
  command: 'npx',
576
- args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')], // Re-use fixture
688
+ args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')], // Re-use fixture
577
689
  logger: sharedLoggerFn,
578
690
  env: { FAKE_CREDS: 'serverY-creds' },
579
691
  },
@@ -1,6 +1,6 @@
1
1
  import { MastraBase } from '@mastra/core/base';
2
2
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
- import type { Resource } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { Resource, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
4
4
  import equal from 'fast-deep-equal';
5
5
  import { v5 as uuidv5 } from 'uuid';
6
6
  import { InternalMastraMCPClient } from './client';
@@ -35,6 +35,7 @@ export class MCPClient extends MastraBase {
35
35
  const existingInstance = mcpClientInstances.get(this.id);
36
36
  if (existingInstance) {
37
37
  void existingInstance.disconnect();
38
+ mcpClientInstances.delete(this.id);
38
39
  }
39
40
  }
40
41
  } else {
@@ -63,6 +64,56 @@ To fix this you have three different options:
63
64
  return this;
64
65
  }
65
66
 
67
+ public get resources() {
68
+ this.addToInstanceCache();
69
+ return {
70
+ list: async (): Promise<Record<string, Resource[]>> => {
71
+ const allResources: Record<string, Resource[]> = {};
72
+ for (const serverName of Object.keys(this.serverConfigs)) {
73
+ try {
74
+ const internalClient = await this.getConnectedClientForServer(serverName);
75
+ allResources[serverName] = await internalClient.resources.list();
76
+ } catch (error) {
77
+ this.logger.error(`Failed to list resources from server ${serverName}`, { error });
78
+ }
79
+ }
80
+ return allResources;
81
+ },
82
+ templates: async (): Promise<Record<string, ResourceTemplate[]>> => {
83
+ const allTemplates: Record<string, ResourceTemplate[]> = {};
84
+ for (const serverName of Object.keys(this.serverConfigs)) {
85
+ try {
86
+ const internalClient = await this.getConnectedClientForServer(serverName);
87
+ allTemplates[serverName] = await internalClient.resources.templates();
88
+ } catch (error) {
89
+ this.logger.error(`Failed to list resource templates from server ${serverName}`, { error });
90
+ }
91
+ }
92
+ return allTemplates;
93
+ },
94
+ read: async (serverName: string, uri: string) => {
95
+ const internalClient = await this.getConnectedClientForServer(serverName);
96
+ return internalClient.resources.read(uri);
97
+ },
98
+ subscribe: async (serverName: string, uri: string) => {
99
+ const internalClient = await this.getConnectedClientForServer(serverName);
100
+ return internalClient.resources.subscribe(uri);
101
+ },
102
+ unsubscribe: async (serverName: string, uri: string) => {
103
+ const internalClient = await this.getConnectedClientForServer(serverName);
104
+ return internalClient.resources.unsubscribe(uri);
105
+ },
106
+ onUpdated: async (serverName: string, handler: (params: { uri: string }) => void) => {
107
+ const internalClient = await this.getConnectedClientForServer(serverName);
108
+ return internalClient.resources.onUpdated(handler);
109
+ },
110
+ onListChanged: async (serverName: string, handler: () => void) => {
111
+ const internalClient = await this.getConnectedClientForServer(serverName);
112
+ return internalClient.resources.onListChanged(handler);
113
+ },
114
+ };
115
+ }
116
+
66
117
  private addToInstanceCache() {
67
118
  if (!mcpClientInstances.has(this.id)) {
68
119
  mcpClientInstances.set(this.id, this);
@@ -86,7 +137,7 @@ To fix this you have three different options:
86
137
  this.disconnectPromise = (async () => {
87
138
  try {
88
139
  mcpClientInstances.delete(this.id);
89
-
140
+
90
141
  // Disconnect all clients in the cache
91
142
  await Promise.all(Array.from(this.mcpClientsById.values()).map(client => client.disconnect()));
92
143
  this.mcpClientsById.clear();
@@ -125,20 +176,10 @@ To fix this you have three different options:
125
176
  }
126
177
 
127
178
  /**
128
- * Get all resources from connected MCP servers
129
- * @returns A record of server names to their resources
179
+ * @deprecated all resource actions have been moved to the this.resources object. Use this.resources.list() instead.
130
180
  */
131
181
  public async getResources() {
132
- this.addToInstanceCache();
133
- const connectedResources: Record<string, Resource[]> = {};
134
-
135
- await this.eachClientResources(async ({ serverName, resources }) => {
136
- if (resources && Array.isArray(resources)) {
137
- connectedResources[serverName] = resources;
138
- }
139
- });
140
-
141
- return connectedResources;
182
+ return this.resources.list();
142
183
  }
143
184
 
144
185
  /**
@@ -155,9 +196,7 @@ To fix this you have three different options:
155
196
  return sessionIds;
156
197
  }
157
198
 
158
- private async getConnectedClient(name: string, config: MastraMCPServerDefinition) {
159
- // Helps to prevent race condition.
160
- // If we want to call connect() we need to wait for the disconnect to complete first if any is ongoing.
199
+ private async getConnectedClient(name: string, config: MastraMCPServerDefinition): Promise<InternalMastraMCPClient> {
161
200
  if (this.disconnectPromise) {
162
201
  await this.disconnectPromise;
163
202
  }
@@ -197,12 +236,18 @@ To fix this you have three different options:
197
236
  `Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
198
237
  );
199
238
  }
200
-
201
239
  this.logger.debug(`Connected to ${name} MCP server`);
202
-
203
240
  return mcpClient;
204
241
  }
205
242
 
243
+ private async getConnectedClientForServer(serverName: string): Promise<InternalMastraMCPClient> {
244
+ const serverConfig = this.serverConfigs[serverName];
245
+ if (!serverConfig) {
246
+ throw new Error(`Server configuration not found for name: ${serverName}`);
247
+ }
248
+ return this.getConnectedClient(serverName, serverConfig);
249
+ }
250
+
206
251
  private async eachClientTools(
207
252
  cb: (args: {
208
253
  serverName: string;
@@ -218,35 +263,6 @@ To fix this you have three different options:
218
263
  }),
219
264
  );
220
265
  }
221
-
222
- /**
223
- * Helper method to iterate through each connected MCP client and retrieve resources
224
- * @param cb Callback function to process resources from each server
225
- */
226
- private async eachClientResources(
227
- cb: (args: {
228
- serverName: string;
229
- resources: Resource[];
230
- client: InstanceType<typeof InternalMastraMCPClient>;
231
- }) => Promise<void>,
232
- ) {
233
- await Promise.all(
234
- Object.entries(this.serverConfigs).map(async ([serverName, serverConfig]) => {
235
- try {
236
- const client = await this.getConnectedClient(serverName, serverConfig);
237
- const response = await client.resources();
238
- // Ensure response has the expected structure
239
- if (response && 'resources' in response && Array.isArray(response.resources)) {
240
- await cb({ serverName, resources: response.resources, client });
241
- }
242
- } catch (e) {
243
- this.logger.error(`Error getting resources from server ${serverName}`, {
244
- error: e instanceof Error ? e.message : String(e),
245
- });
246
- }
247
- }),
248
- );
249
- }
250
266
  }
251
267
 
252
268
  /**
@@ -0,0 +1,121 @@
1
+ import type { IMastraLogger } from "@mastra/core/logger";
2
+ import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { Resource, ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
4
+ import type { InternalMastraMCPClient } from "./client";
5
+
6
+ interface ResourceClientActionsConfig {
7
+ client: InternalMastraMCPClient;
8
+ logger: IMastraLogger;
9
+ }
10
+
11
+ export class ResourceClientActions {
12
+ private readonly client: InternalMastraMCPClient;
13
+ private readonly logger: IMastraLogger;
14
+
15
+ constructor({ client, logger }: ResourceClientActionsConfig) {
16
+ this.client = client;
17
+ this.logger = logger;
18
+ }
19
+
20
+ /**
21
+ * Get all resources from the connected MCP server.
22
+ * @returns A list of resources.
23
+ */
24
+ public async list(): Promise<Resource[]> {
25
+ try {
26
+ const response = await this.client.listResources();
27
+ if (response && response.resources && Array.isArray(response.resources)) {
28
+ return response.resources;
29
+ } else {
30
+ this.logger.warn(`Resources response from server ${this.client.name} did not have expected structure.`, {
31
+ response,
32
+ });
33
+ return [];
34
+ }
35
+ } catch (e: any) {
36
+ // MCP Server might not support resources, so we return an empty array
37
+ if (e.code === ErrorCode.MethodNotFound) {
38
+ return []
39
+ }
40
+ this.logger.error(`Error getting resources from server ${this.client.name}`, {
41
+ error: e instanceof Error ? e.message : String(e),
42
+ });
43
+ console.log('errorheere', e)
44
+ throw new Error(
45
+ `Failed to fetch resources from server ${this.client.name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get all resource templates from the connected MCP server.
52
+ * @returns A list of resource templates.
53
+ */
54
+ public async templates(): Promise<ResourceTemplate[]> {
55
+ try {
56
+ const response = await this.client.listResourceTemplates();
57
+ if (response && response.resourceTemplates && Array.isArray(response.resourceTemplates)) {
58
+ return response.resourceTemplates;
59
+ } else {
60
+ this.logger.warn(
61
+ `Resource templates response from server ${this.client.name} did not have expected structure.`,
62
+ { response },
63
+ );
64
+ return [];
65
+ }
66
+ } catch (e: any) {
67
+ // MCP Server might not support resources, so we return an empty array
68
+ console.log({ errorcooode: e.code })
69
+ if (e.code === ErrorCode.MethodNotFound) {
70
+ return []
71
+ }
72
+ this.logger.error(`Error getting resource templates from server ${this.client.name}`, {
73
+ error: e instanceof Error ? e.message : String(e),
74
+ });
75
+ throw new Error(
76
+ `Failed to fetch resource templates from server ${this.client.name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Read a specific resource.
83
+ * @param uri The URI of the resource to read.
84
+ * @returns The resource content.
85
+ */
86
+ public async read(uri: string) {
87
+ return this.client.readResource(uri);
88
+ }
89
+
90
+ /**
91
+ * Subscribe to a specific resource.
92
+ * @param uri The URI of the resource to subscribe to.
93
+ */
94
+ public async subscribe(uri: string) {
95
+ return this.client.subscribeResource(uri);
96
+ }
97
+
98
+ /**
99
+ * Unsubscribe from a specific resource.
100
+ * @param uri The URI of the resource to unsubscribe from.
101
+ */
102
+ public async unsubscribe(uri: string) {
103
+ return this.client.unsubscribeResource(uri);
104
+ }
105
+
106
+ /**
107
+ * Set a notification handler for when a specific resource is updated.
108
+ * @param handler The callback function to handle the notification.
109
+ */
110
+ public async onUpdated(handler: (params: { uri: string }) => void): Promise<void> {
111
+ this.client.setResourceUpdatedNotificationHandler(handler);
112
+ }
113
+
114
+ /**
115
+ * Set a notification handler for when the list of available resources changes.
116
+ * @param handler The callback function to handle the notification.
117
+ */
118
+ public async onListChanged(handler: () => void): Promise<void> {
119
+ this.client.setResourceListChangedNotificationHandler(handler);
120
+ }
121
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { LoggingLevel, LogMessage, LogHandler, MastraMCPServerDefinition } from './client';
2
- export { MastraMCPClient } from './client';
3
- export * from './configuration';
4
- export * from './server';
1
+ export type { LoggingLevel, LogMessage, LogHandler, MastraMCPServerDefinition } from './client/client';
2
+ export { MastraMCPClient } from './client/client';
3
+ export * from './client/configuration';
4
+ export * from './server/server';
@@ -0,0 +1,62 @@
1
+ import type { IMastraLogger } from '@mastra/core/logger';
2
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+
4
+ interface ServerResourceActionsDependencies {
5
+ getSubscriptions: () => Set<string>;
6
+ getLogger: () => IMastraLogger;
7
+ getSdkServer: () => Server;
8
+ clearDefinedResources: () => void;
9
+ clearDefinedResourceTemplates: () => void;
10
+ }
11
+
12
+ export class ServerResourceActions {
13
+ private readonly getSubscriptions: () => Set<string>;
14
+ private readonly getLogger: () => IMastraLogger;
15
+ private readonly getSdkServer: () => Server;
16
+ private readonly clearDefinedResources: () => void;
17
+ private readonly clearDefinedResourceTemplates: () => void;
18
+
19
+ constructor(dependencies: ServerResourceActionsDependencies) {
20
+ this.getSubscriptions = dependencies.getSubscriptions;
21
+ this.getLogger = dependencies.getLogger;
22
+ this.getSdkServer = dependencies.getSdkServer;
23
+ this.clearDefinedResources = dependencies.clearDefinedResources;
24
+ this.clearDefinedResourceTemplates = dependencies.clearDefinedResourceTemplates;
25
+ }
26
+
27
+ /**
28
+ * Checks if any resources have been updated.
29
+ * If the resource is subscribed to by clients, an update notification will be sent.
30
+ */
31
+ public async notifyUpdated({ uri }: { uri: string }): Promise<void> {
32
+ if (this.getSubscriptions().has(uri)) {
33
+ this.getLogger().info(`Sending notifications/resources/updated for externally notified resource: ${uri}`);
34
+ try {
35
+ await this.getSdkServer().sendResourceUpdated({ uri });
36
+ } catch (error) {
37
+ this.getLogger().error('Failed to send resource updated notification:', { error });
38
+ throw error;
39
+ }
40
+ } else {
41
+ this.getLogger().debug(`Resource ${uri} was updated, but no active subscriptions for it.`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Notifies the server that the overall list of available resources has changed.
47
+ * This will clear the internal cache of defined resources and send a list_changed notification to clients.
48
+ */
49
+ public async notifyListChanged(): Promise<void> {
50
+ this.getLogger().info(
51
+ 'Resource list change externally notified. Clearing definedResources and sending notification.',
52
+ );
53
+ this.clearDefinedResources(); // Clear cached resources
54
+ this.clearDefinedResourceTemplates(); // Clear cached resource templates
55
+ try {
56
+ await this.getSdkServer().sendResourceListChanged();
57
+ } catch (error) {
58
+ this.getLogger().error('Failed to send resource list changed notification:', { error });
59
+ throw error;
60
+ }
61
+ }
62
+ }