@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +34 -0
- package/dist/_tsup-dts-rollup.d.cts +332 -40
- package/dist/_tsup-dts-rollup.d.ts +332 -40
- package/dist/index.cjs +599 -45
- package/dist/index.d.cts +14 -9
- package/dist/index.d.ts +14 -9
- package/dist/index.js +604 -50
- package/package.json +4 -3
- package/src/__fixtures__/fire-crawl-complex-schema.ts +1 -1
- package/src/__fixtures__/server-weather.ts +1 -1
- package/src/__fixtures__/weather.ts +122 -190
- package/src/{client.test.ts → client/client.test.ts} +27 -0
- package/src/{client.ts → client/client.ts} +58 -5
- package/src/{configuration.test.ts → client/configuration.test.ts} +133 -21
- package/src/{configuration.ts → client/configuration.ts} +64 -48
- package/src/client/resourceActions.ts +121 -0
- package/src/index.ts +4 -4
- package/src/server/resourceActions.ts +62 -0
- package/src/{server-logging.test.ts → server/server-logging.test.ts} +9 -7
- package/src/server/server.test.ts +1126 -0
- package/src/{server.ts → server/server.ts} +444 -16
- package/src/server.test.ts +0 -467
|
@@ -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 '
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
*
|
|
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.
|
|
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
|
+
}
|