@mastra/mcp 0.10.2 → 0.10.3
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 +8 -8
- package/CHANGELOG.md +31 -0
- package/README.md +32 -0
- package/dist/_tsup-dts-rollup.d.cts +175 -1
- package/dist/_tsup-dts-rollup.d.ts +175 -1
- package/dist/index.cjs +251 -2
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +253 -4
- package/integration-tests/node_modules/.bin/vitest +2 -2
- package/integration-tests/package.json +3 -3
- package/package.json +9 -9
- package/src/__fixtures__/weather.ts +62 -2
- package/src/client/client.test.ts +46 -0
- package/src/client/client.ts +43 -1
- package/src/client/configuration.test.ts +280 -168
- package/src/client/configuration.ts +27 -1
- package/src/client/index.ts +3 -0
- package/src/client/promptActions.ts +70 -0
- package/src/client/resourceActions.ts +0 -2
- package/src/index.ts +2 -4
- package/src/server/index.ts +2 -0
- package/src/server/promptActions.ts +37 -0
- package/src/server/server.test.ts +224 -1
- package/src/server/server.ts +139 -14
- package/src/server/types.ts +30 -0
package/src/client/client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
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
13
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
14
|
-
import type { ClientCapabilities, LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import type { ClientCapabilities, GetPromptResult, ListPromptsResult, LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
|
|
15
15
|
import {
|
|
16
16
|
CallToolResultSchema,
|
|
17
17
|
ListResourcesResultSchema,
|
|
@@ -19,12 +19,16 @@ import {
|
|
|
19
19
|
ResourceListChangedNotificationSchema,
|
|
20
20
|
ResourceUpdatedNotificationSchema,
|
|
21
21
|
ListResourceTemplatesResultSchema,
|
|
22
|
+
ListPromptsResultSchema,
|
|
23
|
+
GetPromptResultSchema,
|
|
24
|
+
PromptListChangedNotificationSchema,
|
|
22
25
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
23
26
|
|
|
24
27
|
import { asyncExitHook, gracefulExit } from 'exit-hook';
|
|
25
28
|
import { z } from 'zod';
|
|
26
29
|
import { convertJsonSchemaToZod } from 'zod-from-json-schema';
|
|
27
30
|
import type { JSONSchema } from 'zod-from-json-schema';
|
|
31
|
+
import { PromptClientActions } from './promptActions';
|
|
28
32
|
import { ResourceClientActions } from './resourceActions';
|
|
29
33
|
|
|
30
34
|
// Re-export MCP SDK LoggingLevel for convenience
|
|
@@ -119,6 +123,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
119
123
|
private transport?: Transport;
|
|
120
124
|
private currentOperationContext: RuntimeContext | null = null;
|
|
121
125
|
public readonly resources: ResourceClientActions;
|
|
126
|
+
public readonly prompts: PromptClientActions;
|
|
122
127
|
|
|
123
128
|
constructor({
|
|
124
129
|
name,
|
|
@@ -148,6 +153,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
148
153
|
this.setupLogging();
|
|
149
154
|
|
|
150
155
|
this.resources = new ResourceClientActions({ client: this, logger: this.logger });
|
|
156
|
+
this.prompts = new PromptClientActions({ client: this, logger: this.logger });
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/**
|
|
@@ -363,6 +369,42 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
363
369
|
});
|
|
364
370
|
}
|
|
365
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Fetch the list of available prompts from the MCP server.
|
|
374
|
+
*/
|
|
375
|
+
async listPrompts(): Promise<ListPromptsResult> {
|
|
376
|
+
this.log('debug', `Requesting prompts from MCP server`);
|
|
377
|
+
return await this.client.request({ method: 'prompts/list' }, ListPromptsResultSchema, {
|
|
378
|
+
timeout: this.timeout,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get a prompt and its dynamic messages from the server.
|
|
384
|
+
* @param name The prompt name
|
|
385
|
+
* @param args Arguments for the prompt
|
|
386
|
+
* @param version (optional) The prompt version to retrieve
|
|
387
|
+
*/
|
|
388
|
+
async getPrompt({ name, args, version }: { name: string; args?: Record<string, any>; version?: string }): Promise<GetPromptResult> {
|
|
389
|
+
this.log('debug', `Requesting prompt from MCP server: ${name}`);
|
|
390
|
+
return await this.client.request(
|
|
391
|
+
{ method: 'prompts/get', params: { name, arguments: args, version } },
|
|
392
|
+
GetPromptResultSchema,
|
|
393
|
+
{ timeout: this.timeout }
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Register a handler to be called when the prompt list changes on the server.
|
|
399
|
+
* Use this to refresh cached prompt lists in the client/UI if needed.
|
|
400
|
+
*/
|
|
401
|
+
setPromptListChangedNotificationHandler(handler: () => void): void {
|
|
402
|
+
this.log('debug', 'Setting prompt list changed notification handler');
|
|
403
|
+
this.client.setNotificationHandler(PromptListChangedNotificationSchema, () => {
|
|
404
|
+
handler();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
366
408
|
setResourceUpdatedNotificationHandler(
|
|
367
409
|
handler: (params: z.infer<typeof ResourceUpdatedNotificationSchema>['params']) => void,
|
|
368
410
|
): void {
|
|
@@ -81,219 +81,317 @@ describe('MCPClient', () => {
|
|
|
81
81
|
weatherProcess.kill('SIGINT');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
describe('Instance Management', () => {
|
|
85
|
+
it('should initialize with server configurations', () => {
|
|
86
|
+
expect(mcp['serverConfigs']).toEqual({
|
|
87
|
+
stockPrice: {
|
|
88
|
+
command: 'npx',
|
|
89
|
+
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
89
90
|
env: {
|
|
90
91
|
FAKE_CREDS: 'test',
|
|
91
92
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
},
|
|
94
|
+
weather: {
|
|
95
|
+
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
96
98
|
});
|
|
97
|
-
});
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
it('should get connected tools with namespaced tool names', async () => {
|
|
101
|
+
const connectedTools = await mcp.getTools();
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// Each tool should be namespaced with its server name
|
|
104
|
+
expect(connectedTools).toHaveProperty('stockPrice_getStockPrice');
|
|
105
|
+
expect(connectedTools).toHaveProperty('weather_getWeather');
|
|
106
|
+
});
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
it('should get connected toolsets grouped by server', async () => {
|
|
109
|
+
const connectedToolsets = await mcp.getToolsets();
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
expect(connectedToolsets).toHaveProperty('stockPrice');
|
|
112
|
+
expect(connectedToolsets).toHaveProperty('weather');
|
|
113
|
+
expect(connectedToolsets.stockPrice).toHaveProperty('getStockPrice');
|
|
114
|
+
expect(connectedToolsets.weather).toHaveProperty('getWeather');
|
|
115
|
+
});
|
|
114
116
|
});
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
118
|
+
describe('Resources', () => {
|
|
119
|
+
it('should get resources from connected MCP servers', async () => {
|
|
120
|
+
const resources = await mcp.resources.list();
|
|
121
|
+
|
|
122
|
+
expect(resources).toHaveProperty('weather');
|
|
123
|
+
expect(resources.weather).toBeDefined();
|
|
124
|
+
expect(resources.weather).toHaveLength(3);
|
|
125
|
+
|
|
126
|
+
// Verify that each expected resource exists with the correct structure
|
|
127
|
+
const weatherResources = resources.weather;
|
|
128
|
+
const currentWeather = weatherResources.find(r => r.uri === 'weather://current');
|
|
129
|
+
expect(currentWeather).toBeDefined();
|
|
130
|
+
expect(currentWeather).toMatchObject({
|
|
131
|
+
uri: 'weather://current',
|
|
132
|
+
name: 'Current Weather Data',
|
|
133
|
+
description: expect.any(String),
|
|
134
|
+
mimeType: 'application/json',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const forecast = weatherResources.find(r => r.uri === 'weather://forecast');
|
|
138
|
+
expect(forecast).toBeDefined();
|
|
139
|
+
expect(forecast).toMatchObject({
|
|
140
|
+
uri: 'weather://forecast',
|
|
141
|
+
name: 'Weather Forecast',
|
|
142
|
+
description: expect.any(String),
|
|
143
|
+
mimeType: 'application/json',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const historical = weatherResources.find(r => r.uri === 'weather://historical');
|
|
147
|
+
expect(historical).toBeDefined();
|
|
148
|
+
expect(historical).toMatchObject({
|
|
149
|
+
uri: 'weather://historical',
|
|
150
|
+
name: 'Historical Weather Data',
|
|
151
|
+
description: expect.any(String),
|
|
152
|
+
mimeType: 'application/json',
|
|
153
|
+
});
|
|
132
154
|
});
|
|
133
155
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
it('should list resource templates from connected MCP servers', async () => {
|
|
157
|
+
const templates = await mcp.resources.templates();
|
|
158
|
+
expect(templates).toHaveProperty('weather');
|
|
159
|
+
expect(templates.weather).toBeDefined();
|
|
160
|
+
expect(templates.weather.length).toBeGreaterThan(0);
|
|
161
|
+
const customForecastTemplate = templates.weather.find(
|
|
162
|
+
(t: ResourceTemplate) => t.uriTemplate === 'weather://custom/{city}/{days}',
|
|
163
|
+
);
|
|
164
|
+
expect(customForecastTemplate).toBeDefined();
|
|
165
|
+
expect(customForecastTemplate).toMatchObject({
|
|
166
|
+
uriTemplate: 'weather://custom/{city}/{days}',
|
|
167
|
+
name: 'Custom Weather Forecast',
|
|
168
|
+
description: expect.any(String),
|
|
169
|
+
mimeType: 'application/json',
|
|
170
|
+
});
|
|
141
171
|
});
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
it('should read a specific resource from a server', async () => {
|
|
174
|
+
const resourceContent = await mcp.resources.read('weather', 'weather://current');
|
|
175
|
+
expect(resourceContent).toBeDefined();
|
|
176
|
+
expect(resourceContent.contents).toBeInstanceOf(Array);
|
|
177
|
+
expect(resourceContent.contents.length).toBe(1);
|
|
178
|
+
const contentItem = resourceContent.contents[0];
|
|
179
|
+
expect(contentItem.uri).toBe('weather://current');
|
|
180
|
+
expect(contentItem.mimeType).toBe('application/json');
|
|
181
|
+
expect(contentItem.text).toBeDefined();
|
|
182
|
+
let parsedText: any = {};
|
|
183
|
+
if (contentItem.text && typeof contentItem.text === 'string') {
|
|
184
|
+
try {
|
|
185
|
+
parsedText = JSON.parse(contentItem.text);
|
|
186
|
+
} catch {
|
|
187
|
+
// If parsing fails, parsedText remains an empty object
|
|
188
|
+
// console.error("Failed to parse resource content text:", _e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
expect(parsedText).toHaveProperty('location');
|
|
150
192
|
});
|
|
151
|
-
});
|
|
152
193
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(customForecastTemplate).toMatchObject({
|
|
163
|
-
uriTemplate: 'weather://custom/{city}/{days}',
|
|
164
|
-
name: 'Custom Weather Forecast',
|
|
165
|
-
description: expect.any(String),
|
|
166
|
-
mimeType: 'application/json',
|
|
194
|
+
it('should subscribe and unsubscribe from a resource on a specific server', async () => {
|
|
195
|
+
const serverName = 'weather';
|
|
196
|
+
const resourceUri = 'weather://current';
|
|
197
|
+
|
|
198
|
+
const subResult = await mcp.resources.subscribe(serverName, resourceUri);
|
|
199
|
+
expect(subResult).toEqual({});
|
|
200
|
+
|
|
201
|
+
const unsubResult = await mcp.resources.unsubscribe(serverName, resourceUri);
|
|
202
|
+
expect(unsubResult).toEqual({});
|
|
167
203
|
});
|
|
168
|
-
});
|
|
169
204
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
205
|
+
it('should receive resource updated notification from a specific server', async () => {
|
|
206
|
+
const serverName = 'weather';
|
|
207
|
+
const resourceUri = 'weather://current';
|
|
208
|
+
let notificationReceived = false;
|
|
209
|
+
let receivedUri = '';
|
|
210
|
+
|
|
211
|
+
await mcp.resources.list(); // Initial call to establish connection if needed
|
|
212
|
+
// Create the promise for the notification BEFORE subscribing
|
|
213
|
+
const resourceUpdatedPromise = new Promise<void>((resolve, reject) => {
|
|
214
|
+
mcp.resources.onUpdated(serverName, (params: { uri: string }) => {
|
|
215
|
+
if (params.uri === resourceUri) {
|
|
216
|
+
notificationReceived = true;
|
|
217
|
+
receivedUri = params.uri;
|
|
218
|
+
resolve();
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`[Test LOG] Received update for ${params.uri}, waiting for ${resourceUri}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
setTimeout(() => reject(new Error(`Timeout waiting for resourceUpdated notification for ${resourceUri}`)), 4500);
|
|
224
|
+
});
|
|
190
225
|
|
|
191
|
-
|
|
192
|
-
const serverName = 'weather';
|
|
193
|
-
const resourceUri = 'weather://current';
|
|
226
|
+
await mcp.resources.subscribe(serverName, resourceUri); // Ensure subscription is active
|
|
194
227
|
|
|
195
|
-
|
|
196
|
-
expect(subResult).toEqual({});
|
|
228
|
+
await expect(resourceUpdatedPromise).resolves.toBeUndefined(); // Wait for the notification
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
expect(notificationReceived).toBe(true);
|
|
231
|
+
expect(receivedUri).toBe(resourceUri);
|
|
232
|
+
|
|
233
|
+
await mcp.resources.unsubscribe(serverName, resourceUri); // Cleanup
|
|
234
|
+
}, 5000);
|
|
235
|
+
|
|
236
|
+
it('should receive resource list changed notification from a specific server', async () => {
|
|
237
|
+
const serverName = 'weather';
|
|
238
|
+
let notificationReceived = false;
|
|
201
239
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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) {
|
|
240
|
+
await mcp.resources.list(); // Initial call to establish connection
|
|
241
|
+
|
|
242
|
+
const resourceListChangedPromise = new Promise<void>((resolve, reject) => {
|
|
243
|
+
mcp.resources.onListChanged(serverName, () => {
|
|
213
244
|
notificationReceived = true;
|
|
214
|
-
receivedUri = params.uri;
|
|
215
245
|
resolve();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
}
|
|
246
|
+
});
|
|
247
|
+
setTimeout(() => reject(new Error('Timeout waiting for resourceListChanged notification')), 4500);
|
|
219
248
|
});
|
|
220
|
-
|
|
249
|
+
|
|
250
|
+
// In a real scenario, something would trigger the server to send this.
|
|
251
|
+
// For the test, we rely on the interval in weather.ts or a direct call if available.
|
|
252
|
+
// Adding a small delay or an explicit trigger if the fixture supported it would be more robust.
|
|
253
|
+
// For now, we assume the interval in weather.ts will eventually fire it.
|
|
254
|
+
|
|
255
|
+
await expect(resourceListChangedPromise).resolves.toBeUndefined(); // Wait for the notification
|
|
256
|
+
|
|
257
|
+
expect(notificationReceived).toBe(true);
|
|
221
258
|
});
|
|
222
259
|
|
|
223
|
-
|
|
260
|
+
it('should handle errors when getting resources', async () => {
|
|
261
|
+
const errorClient = new MCPClient({
|
|
262
|
+
id: 'error-test-client',
|
|
263
|
+
servers: {
|
|
264
|
+
weather: {
|
|
265
|
+
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
266
|
+
},
|
|
267
|
+
nonexistentServer: {
|
|
268
|
+
command: 'nonexistent-command',
|
|
269
|
+
args: [],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
});
|
|
224
273
|
|
|
225
|
-
|
|
274
|
+
try {
|
|
275
|
+
const resources = await errorClient.resources.list();
|
|
226
276
|
|
|
227
|
-
|
|
228
|
-
|
|
277
|
+
expect(resources).toHaveProperty('weather');
|
|
278
|
+
expect(resources.weather).toBeDefined();
|
|
279
|
+
expect(resources.weather.length).toBeGreaterThan(0);
|
|
229
280
|
|
|
230
|
-
|
|
231
|
-
|
|
281
|
+
expect(resources).not.toHaveProperty('nonexistentServer');
|
|
282
|
+
} finally {
|
|
283
|
+
await errorClient.disconnect();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('Prompts', () => {
|
|
289
|
+
it('should get prompts from connected MCP servers', async () => {
|
|
290
|
+
const prompts = await mcp.prompts.list();
|
|
291
|
+
|
|
292
|
+
expect(prompts).toHaveProperty('weather');
|
|
293
|
+
expect(prompts['weather']).toBeDefined();
|
|
294
|
+
expect(prompts['weather']).toHaveLength(3);
|
|
295
|
+
|
|
296
|
+
// Verify that each expected resource exists with the correct structure
|
|
297
|
+
const promptResources = prompts['weather'];
|
|
298
|
+
const currentWeatherPrompt = promptResources.find(r => r.name === 'current');
|
|
299
|
+
expect(currentWeatherPrompt).toBeDefined();
|
|
300
|
+
expect(currentWeatherPrompt).toMatchObject({
|
|
301
|
+
name: 'current',
|
|
302
|
+
version: 'v1',
|
|
303
|
+
description: expect.any(String),
|
|
304
|
+
mimeType: 'application/json',
|
|
305
|
+
});
|
|
232
306
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
const forecast = promptResources.find(r => r.name === 'forecast');
|
|
308
|
+
expect(forecast).toBeDefined();
|
|
309
|
+
expect(forecast).toMatchObject({
|
|
310
|
+
name: 'forecast',
|
|
311
|
+
version: 'v1',
|
|
312
|
+
description: expect.any(String),
|
|
313
|
+
mimeType: 'application/json',
|
|
314
|
+
});
|
|
236
315
|
|
|
237
|
-
|
|
316
|
+
const historical = promptResources.find(r => r.name === 'historical');
|
|
317
|
+
expect(historical).toBeDefined();
|
|
318
|
+
expect(historical).toMatchObject({
|
|
319
|
+
name: 'historical',
|
|
320
|
+
version: 'v1',
|
|
321
|
+
description: expect.any(String),
|
|
322
|
+
mimeType: 'application/json',
|
|
323
|
+
});
|
|
324
|
+
});
|
|
238
325
|
|
|
239
|
-
|
|
240
|
-
mcp.
|
|
241
|
-
|
|
242
|
-
|
|
326
|
+
it('should get a specific prompt from a server', async () => {
|
|
327
|
+
const {prompt, messages} = await mcp.prompts.get({serverName: 'weather', name: 'current'});
|
|
328
|
+
expect(prompt).toBeDefined();
|
|
329
|
+
expect(prompt).toMatchObject({
|
|
330
|
+
name: 'current',
|
|
331
|
+
version: 'v1',
|
|
332
|
+
description: expect.any(String),
|
|
333
|
+
mimeType: 'application/json',
|
|
243
334
|
});
|
|
244
|
-
|
|
335
|
+
expect(messages).toBeDefined();
|
|
336
|
+
const messageItem = messages[0];
|
|
337
|
+
let parsedText: any = {};
|
|
338
|
+
if (messageItem.content.text && typeof messageItem.content.text === 'string') {
|
|
339
|
+
try {
|
|
340
|
+
parsedText = JSON.parse(messageItem.content.text);
|
|
341
|
+
} catch {
|
|
342
|
+
// If parsing fails, parsedText remains an empty object
|
|
343
|
+
// console.error("Failed to parse resource content text:", _e);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
expect(parsedText).toHaveProperty('location');
|
|
245
347
|
});
|
|
246
348
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// For now, we assume the interval in weather.ts will eventually fire it.
|
|
349
|
+
it('should receive prompt list changed notification from a specific server', async () => {
|
|
350
|
+
const serverName = 'weather';
|
|
351
|
+
let notificationReceived = false;
|
|
251
352
|
|
|
252
|
-
|
|
353
|
+
await mcp.prompts.list();
|
|
253
354
|
|
|
254
|
-
|
|
255
|
-
|
|
355
|
+
const promptListChangedPromise = new Promise<void>((resolve, reject) => {
|
|
356
|
+
mcp.prompts.onListChanged(serverName, () => {
|
|
357
|
+
notificationReceived = true;
|
|
358
|
+
resolve();
|
|
359
|
+
});
|
|
360
|
+
setTimeout(() => reject(new Error('Timeout waiting for promptListChanged notification')), 4500);
|
|
361
|
+
});
|
|
256
362
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
servers: {
|
|
261
|
-
weather: {
|
|
262
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
263
|
-
},
|
|
264
|
-
nonexistentServer: {
|
|
265
|
-
command: 'nonexistent-command',
|
|
266
|
-
args: [],
|
|
267
|
-
},
|
|
268
|
-
},
|
|
363
|
+
await expect(promptListChangedPromise).resolves.toBeUndefined();
|
|
364
|
+
|
|
365
|
+
expect(notificationReceived).toBe(true);
|
|
269
366
|
});
|
|
270
367
|
|
|
271
|
-
|
|
272
|
-
const
|
|
368
|
+
it('should handle errors when getting prompts', async () => {
|
|
369
|
+
const errorClient = new MCPClient({
|
|
370
|
+
id: 'error-test-client',
|
|
371
|
+
servers: {
|
|
372
|
+
weather: {
|
|
373
|
+
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
374
|
+
},
|
|
375
|
+
nonexistentServer: {
|
|
376
|
+
command: 'nonexistent-command',
|
|
377
|
+
args: [],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
273
381
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
expect(resources.weather.length).toBeGreaterThan(0);
|
|
382
|
+
try {
|
|
383
|
+
const prompts = await errorClient.prompts.list();
|
|
277
384
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
});
|
|
385
|
+
expect(prompts).toHaveProperty('weather');
|
|
386
|
+
expect(prompts['weather']).toBeDefined();
|
|
387
|
+
expect(prompts['weather'].length).toBeGreaterThan(0);
|
|
283
388
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
command: 'nonexistent-command',
|
|
289
|
-
args: [],
|
|
290
|
-
},
|
|
291
|
-
},
|
|
389
|
+
expect(prompts).not.toHaveProperty('nonexistentServer');
|
|
390
|
+
} finally {
|
|
391
|
+
await errorClient.disconnect();
|
|
392
|
+
}
|
|
292
393
|
});
|
|
293
|
-
|
|
294
|
-
await expect(badConfig.getTools()).rejects.toThrow();
|
|
295
|
-
await badConfig.disconnect();
|
|
296
|
-
});
|
|
394
|
+
})
|
|
297
395
|
|
|
298
396
|
describe('Instance Management', () => {
|
|
299
397
|
it('should allow multiple instances with different IDs', async () => {
|
|
@@ -479,6 +577,20 @@ describe('MCPClient', () => {
|
|
|
479
577
|
await expect(mixedConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
480
578
|
await mixedConfig.disconnect();
|
|
481
579
|
});
|
|
580
|
+
|
|
581
|
+
it('should handle connection errors gracefully', async () => {
|
|
582
|
+
const badConfig = new MCPClient({
|
|
583
|
+
servers: {
|
|
584
|
+
badServer: {
|
|
585
|
+
command: 'nonexistent-command',
|
|
586
|
+
args: [],
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await expect(badConfig.getTools()).rejects.toThrow();
|
|
592
|
+
await badConfig.disconnect();
|
|
593
|
+
});
|
|
482
594
|
});
|
|
483
595
|
|
|
484
596
|
describe('Schema Handling', () => {
|
|
@@ -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, ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { Prompt, 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';
|
|
@@ -114,6 +114,32 @@ To fix this you have three different options:
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
public get prompts() {
|
|
118
|
+
this.addToInstanceCache();
|
|
119
|
+
return {
|
|
120
|
+
list: async (): Promise<Record<string, Prompt[]>> => {
|
|
121
|
+
const allPrompts: Record<string, Prompt[]> = {};
|
|
122
|
+
for (const serverName of Object.keys(this.serverConfigs)) {
|
|
123
|
+
try {
|
|
124
|
+
const internalClient = await this.getConnectedClientForServer(serverName);
|
|
125
|
+
allPrompts[serverName] = await internalClient.prompts.list();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.logger.error(`Failed to list prompts from server ${serverName}`, { error });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return allPrompts;
|
|
131
|
+
},
|
|
132
|
+
get: async ({serverName, name, args, version}: {serverName: string, name: string, args?: Record<string, any>, version?: string}) => {
|
|
133
|
+
const internalClient = await this.getConnectedClientForServer(serverName);
|
|
134
|
+
return internalClient.prompts.get({name, args, version});
|
|
135
|
+
},
|
|
136
|
+
onListChanged: async (serverName: string, handler: () => void) => {
|
|
137
|
+
const internalClient = await this.getConnectedClientForServer(serverName);
|
|
138
|
+
return internalClient.prompts.onListChanged(handler);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
private addToInstanceCache() {
|
|
118
144
|
if (!mcpClientInstances.has(this.id)) {
|
|
119
145
|
mcpClientInstances.set(this.id, this);
|