@mastra/mcp 0.11.3-alpha.1 → 0.11.3-alpha.2

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +16 -3
  3. package/.turbo/turbo-build.log +0 -4
  4. package/eslint.config.js +0 -11
  5. package/integration-tests/node_modules/.bin/tsc +0 -21
  6. package/integration-tests/node_modules/.bin/tsserver +0 -21
  7. package/integration-tests/node_modules/.bin/vitest +0 -21
  8. package/integration-tests/package.json +0 -29
  9. package/integration-tests/src/mastra/agents/weather.ts +0 -34
  10. package/integration-tests/src/mastra/index.ts +0 -15
  11. package/integration-tests/src/mastra/mcp/index.ts +0 -46
  12. package/integration-tests/src/mastra/tools/weather.ts +0 -13
  13. package/integration-tests/src/server.test.ts +0 -238
  14. package/integration-tests/tsconfig.json +0 -13
  15. package/integration-tests/vitest.config.ts +0 -14
  16. package/src/__fixtures__/fire-crawl-complex-schema.ts +0 -1013
  17. package/src/__fixtures__/server-weather.ts +0 -16
  18. package/src/__fixtures__/stock-price.ts +0 -128
  19. package/src/__fixtures__/tools.ts +0 -94
  20. package/src/__fixtures__/weather.ts +0 -269
  21. package/src/client/client.test.ts +0 -585
  22. package/src/client/client.ts +0 -628
  23. package/src/client/configuration.test.ts +0 -856
  24. package/src/client/configuration.ts +0 -468
  25. package/src/client/elicitationActions.ts +0 -26
  26. package/src/client/index.ts +0 -3
  27. package/src/client/promptActions.ts +0 -70
  28. package/src/client/resourceActions.ts +0 -119
  29. package/src/index.ts +0 -2
  30. package/src/server/index.ts +0 -2
  31. package/src/server/promptActions.ts +0 -48
  32. package/src/server/resourceActions.ts +0 -90
  33. package/src/server/server-logging.test.ts +0 -181
  34. package/src/server/server.test.ts +0 -2142
  35. package/src/server/server.ts +0 -1445
  36. package/src/server/types.ts +0 -59
  37. package/tsconfig.build.json +0 -9
  38. package/tsconfig.json +0 -5
  39. package/tsup.config.ts +0 -17
  40. package/vitest.config.ts +0 -8
@@ -1,2142 +0,0 @@
1
- import http from 'node:http';
2
- import path from 'path';
3
- import type { ServerType } from '@hono/node-server';
4
- import { serve } from '@hono/node-server';
5
- import { Agent } from '@mastra/core/agent';
6
- import type { ToolsInput } from '@mastra/core/agent';
7
- import type { MCPServerConfig, Repository, PackageInfo, RemoteInfo, ConvertedTool } from '@mastra/core/mcp';
8
- import { createStep, Workflow } from '@mastra/core/workflows';
9
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
- import type {
11
- Resource,
12
- ResourceTemplate,
13
- ListResourcesResult,
14
- ReadResourceResult,
15
- ListResourceTemplatesResult,
16
- GetPromptResult,
17
- Prompt,
18
- } from '@modelcontextprotocol/sdk/types.js';
19
- import { MockLanguageModelV1 } from 'ai/test';
20
- import { Hono } from 'hono';
21
- import { describe, it, expect, beforeAll, afterAll, afterEach, vi, beforeEach } from 'vitest';
22
- import { z } from 'zod';
23
- import { weatherTool } from '../__fixtures__/tools';
24
- import { InternalMastraMCPClient } from '../client/client';
25
- import { MCPClient } from '../client/configuration';
26
- import { MCPServer } from './server';
27
- import type { MCPServerResources, MCPServerResourceContent, MCPRequestHandlerExtra } from './types';
28
-
29
- const PORT = 9100 + Math.floor(Math.random() * 1000);
30
- let server: MCPServer;
31
- let httpServer: http.Server;
32
-
33
- vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 });
34
-
35
- // Mock Date constructor for predictable release dates
36
- const mockDateISO = '2024-01-01T00:00:00.000Z';
37
- const mockDate = new Date(mockDateISO);
38
- const OriginalDate = global.Date; // Store original Date
39
-
40
- // Mock a simple tool
41
- const mockToolExecute = vi.fn(async (args: any) => ({ result: 'tool executed', args }));
42
- const mockTools: ToolsInput = {
43
- testTool: {
44
- description: 'A test tool',
45
- parameters: z.object({ input: z.string().optional() }),
46
- execute: mockToolExecute,
47
- },
48
- };
49
-
50
- const minimalTestTool: ToolsInput = {
51
- minTool: {
52
- description: 'A minimal tool',
53
- parameters: z.object({}),
54
- execute: async () => ({ result: 'ok' }),
55
- },
56
- };
57
-
58
- const mockAgentGenerate = vi.fn(async (query: string) => {
59
- return {
60
- rawCall: { rawPrompt: null, rawSettings: {} },
61
- finishReason: 'stop',
62
- usage: { promptTokens: 10, completionTokens: 20 },
63
- text: `{"content":"Agent response to: "${JSON.stringify(query)}"}`,
64
- };
65
- });
66
-
67
- const mockAgentGetInstructions = vi.fn(() => 'This is a mock agent for testing.');
68
-
69
- const createMockAgent = (name: string, generateFn: any, instructionsFn?: any, description?: string) => {
70
- return new Agent({
71
- name: name,
72
- instructions: instructionsFn,
73
- description: description || '',
74
- model: new MockLanguageModelV1({
75
- defaultObjectGenerationMode: 'json',
76
- doGenerate: async options => {
77
- return generateFn((options.prompt.at(-1)?.content[0] as { text: string }).text);
78
- },
79
- }),
80
- });
81
- };
82
-
83
- const createMockWorkflow = (
84
- id: string,
85
- description?: string,
86
- inputSchema?: z.ZodTypeAny,
87
- outputSchema?: z.ZodTypeAny,
88
- ) => {
89
- return new Workflow({
90
- id,
91
- description: description || '',
92
- inputSchema: inputSchema as z.ZodType<any>,
93
- outputSchema: outputSchema as z.ZodType<any>,
94
- steps: [],
95
- });
96
- };
97
-
98
- const minimalConfig: MCPServerConfig = {
99
- name: 'TestServer',
100
- version: '1.0.0',
101
- tools: mockTools,
102
- };
103
-
104
- describe('MCPServer', () => {
105
- beforeEach(() => {
106
- vi.clearAllMocks();
107
-
108
- // @ts-ignore - Mocking Date completely
109
- global.Date = vi.fn((...args: any[]) => {
110
- if (args.length === 0) {
111
- // new Date()
112
- return mockDate;
113
- }
114
- // @ts-ignore
115
- return new OriginalDate(...args); // new Date('some-string') or new Date(timestamp)
116
- }) as any;
117
-
118
- // @ts-ignore
119
- global.Date.now = vi.fn(() => mockDate.getTime());
120
- // @ts-ignore
121
- global.Date.prototype.toISOString = vi.fn(() => mockDateISO);
122
- // @ts-ignore // Static Date.toISOString() might be used by some libraries
123
- global.Date.toISOString = vi.fn(() => mockDateISO);
124
- });
125
-
126
- // Restore original Date after all tests in this describe block
127
- afterAll(() => {
128
- global.Date = OriginalDate;
129
- });
130
-
131
- describe('Constructor and Metadata Initialization', () => {
132
- it('should initialize with default metadata if not provided', () => {
133
- const server = new MCPServer(minimalConfig);
134
- expect(server.id).toBeDefined();
135
- expect(server.name).toBe('TestServer');
136
- expect(server.version).toBe('1.0.0');
137
- expect(server.description).toBeUndefined();
138
- expect(server.repository).toBeUndefined();
139
- // MCPServerBase stores releaseDate as string, compare directly or re-parse
140
- expect(server.releaseDate).toBe(mockDateISO);
141
- expect(server.isLatest).toBe(true);
142
- expect(server.packageCanonical).toBeUndefined();
143
- expect(server.packages).toBeUndefined();
144
- expect(server.remotes).toBeUndefined();
145
- });
146
-
147
- it('should initialize with custom metadata when provided', () => {
148
- const repository: Repository = { url: 'https://github.com/test/repo', source: 'github', id: 'repo-id' };
149
- const packages: PackageInfo[] = [{ registry_name: 'npm', name: 'test-package', version: '1.0.0' }];
150
- const remotes: RemoteInfo[] = [{ transport_type: 'sse', url: 'https://test.com/sse' }];
151
- const customReleaseDate = '2023-12-31T00:00:00.000Z';
152
- const customConfig: MCPServerConfig = {
153
- ...minimalConfig,
154
- id: 'custom-id-doesnt-need-uuid-format-if-set-explicitly',
155
- description: 'A custom server description',
156
- repository,
157
- releaseDate: customReleaseDate,
158
- isLatest: false,
159
- packageCanonical: 'npm',
160
- packages,
161
- remotes,
162
- };
163
- const server = new MCPServer(customConfig);
164
-
165
- expect(server.id).toBe('custom-id-doesnt-need-uuid-format-if-set-explicitly');
166
- expect(server.description).toBe('A custom server description');
167
- expect(server.repository).toEqual(repository);
168
- expect(server.releaseDate).toBe(customReleaseDate);
169
- expect(server.isLatest).toBe(false);
170
- expect(server.packageCanonical).toBe('npm');
171
- expect(server.packages).toEqual(packages);
172
- expect(server.remotes).toEqual(remotes);
173
- });
174
- });
175
-
176
- describe('getServerInfo()', () => {
177
- it('should return correct ServerInfo with default metadata', () => {
178
- const server = new MCPServer(minimalConfig);
179
- const serverInfo = server.getServerInfo();
180
-
181
- expect(serverInfo).toEqual({
182
- id: expect.any(String),
183
- name: 'TestServer',
184
- description: undefined,
185
- repository: undefined,
186
- version_detail: {
187
- version: '1.0.0',
188
- release_date: mockDateISO,
189
- is_latest: true,
190
- },
191
- });
192
- });
193
-
194
- it('should return correct ServerInfo with custom metadata', () => {
195
- const repository: Repository = { url: 'https://github.com/test/repo', source: 'github', id: 'repo-id' };
196
- const customReleaseDate = '2023-11-01T00:00:00.000Z';
197
- const customConfig: MCPServerConfig = {
198
- ...minimalConfig,
199
- id: 'custom-id-for-info',
200
- description: 'Custom description',
201
- repository,
202
- releaseDate: customReleaseDate,
203
- isLatest: false,
204
- };
205
- const server = new MCPServer(customConfig);
206
- const serverInfo = server.getServerInfo();
207
-
208
- expect(serverInfo).toEqual({
209
- id: 'custom-id-for-info',
210
- name: 'TestServer',
211
- description: 'Custom description',
212
- repository,
213
- version_detail: {
214
- version: '1.0.0',
215
- release_date: customReleaseDate,
216
- is_latest: false,
217
- },
218
- });
219
- });
220
- });
221
-
222
- describe('getServerDetail()', () => {
223
- it('should return correct ServerDetailInfo with default metadata', () => {
224
- const server = new MCPServer(minimalConfig);
225
- const serverDetail = server.getServerDetail();
226
-
227
- expect(serverDetail).toEqual({
228
- id: expect.any(String),
229
- name: 'TestServer',
230
- description: undefined,
231
- repository: undefined,
232
- version_detail: {
233
- version: '1.0.0',
234
- release_date: mockDateISO,
235
- is_latest: true,
236
- },
237
- package_canonical: undefined,
238
- packages: undefined,
239
- remotes: undefined,
240
- });
241
- });
242
-
243
- it('should return correct ServerDetailInfo with custom metadata', () => {
244
- const repository: Repository = { url: 'https://github.com/test/repo', source: 'github', id: 'repo-id' };
245
- const packages: PackageInfo[] = [{ registry_name: 'npm', name: 'test-package', version: '1.0.0' }];
246
- const remotes: RemoteInfo[] = [{ transport_type: 'sse', url: 'https://test.com/sse' }];
247
- const customReleaseDate = '2023-10-01T00:00:00.000Z';
248
- const customConfig: MCPServerConfig = {
249
- ...minimalConfig,
250
- id: 'custom-id-for-detail',
251
- description: 'Custom detail description',
252
- repository,
253
- releaseDate: customReleaseDate,
254
- isLatest: true,
255
- packageCanonical: 'docker',
256
- packages,
257
- remotes,
258
- };
259
- const server = new MCPServer(customConfig);
260
- const serverDetail = server.getServerDetail();
261
-
262
- expect(serverDetail).toEqual({
263
- id: 'custom-id-for-detail',
264
- name: 'TestServer',
265
- description: 'Custom detail description',
266
- repository,
267
- version_detail: {
268
- version: '1.0.0',
269
- release_date: customReleaseDate,
270
- is_latest: true,
271
- },
272
- package_canonical: 'docker',
273
- packages,
274
- remotes,
275
- });
276
- });
277
- });
278
-
279
- describe('MCPServer Resource Handling', () => {
280
- let resourceTestServerInstance: MCPServer;
281
- let localHttpServerForResources: http.Server;
282
- let resourceTestInternalClient: InternalMastraMCPClient;
283
- const RESOURCE_TEST_PORT = 9200 + Math.floor(Math.random() * 1000);
284
-
285
- const mockResourceContents: Record<string, MCPServerResourceContent> = {
286
- 'weather://current': {
287
- text: JSON.stringify({
288
- location: 'Test City',
289
- temperature: 22,
290
- conditions: 'Sunny',
291
- }),
292
- },
293
- 'weather://forecast': {
294
- text: JSON.stringify([
295
- { day: 1, high: 25, low: 15, conditions: 'Clear' },
296
- { day: 2, high: 26, low: 16, conditions: 'Cloudy' },
297
- ]),
298
- },
299
- 'weather://historical': {
300
- text: JSON.stringify({ averageHigh: 20, averageLow: 10 }),
301
- },
302
- };
303
-
304
- const initialResourcesForTest: Resource[] = [
305
- {
306
- uri: 'weather://current',
307
- name: 'Current Weather Data',
308
- description: 'Real-time weather data',
309
- mimeType: 'application/json',
310
- },
311
- {
312
- uri: 'weather://forecast',
313
- name: 'Weather Forecast',
314
- description: '5-day weather forecast',
315
- mimeType: 'application/json',
316
- },
317
- {
318
- uri: 'weather://historical',
319
- name: 'Historical Weather Data',
320
- description: 'Past 30 days weather data',
321
- mimeType: 'application/json',
322
- },
323
- ];
324
-
325
- const mockAppResourcesFunctions: MCPServerResources = {
326
- listResources: async () => initialResourcesForTest,
327
- getResourceContent: async ({ uri }) => {
328
- if (mockResourceContents[uri]) {
329
- return mockResourceContents[uri];
330
- }
331
- throw new Error(`Mock resource content not found for ${uri}`);
332
- },
333
- };
334
-
335
- beforeAll(async () => {
336
- resourceTestServerInstance = new MCPServer({
337
- name: 'ResourceTestServer',
338
- version: '1.0.0',
339
- tools: minimalTestTool,
340
- resources: mockAppResourcesFunctions,
341
- });
342
-
343
- localHttpServerForResources = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
344
- const url = new URL(req.url || '', `http://localhost:${RESOURCE_TEST_PORT}`);
345
- await resourceTestServerInstance.startHTTP({
346
- url,
347
- httpPath: '/http',
348
- req,
349
- res,
350
- options: {
351
- sessionIdGenerator: undefined,
352
- },
353
- });
354
- });
355
-
356
- await new Promise<void>(resolve => localHttpServerForResources.listen(RESOURCE_TEST_PORT, () => resolve()));
357
-
358
- resourceTestInternalClient = new InternalMastraMCPClient({
359
- name: 'resource-test-internal-client',
360
- server: {
361
- url: new URL(`http://localhost:${RESOURCE_TEST_PORT}/http`),
362
- },
363
- });
364
- await resourceTestInternalClient.connect();
365
- });
366
-
367
- afterAll(async () => {
368
- await resourceTestInternalClient.disconnect();
369
- if (localHttpServerForResources) {
370
- localHttpServerForResources.closeAllConnections?.();
371
- await new Promise<void>((resolve, reject) => {
372
- localHttpServerForResources.close(err => {
373
- if (err) return reject(err);
374
- resolve();
375
- });
376
- });
377
- }
378
- if (resourceTestServerInstance) {
379
- await resourceTestServerInstance.close();
380
- }
381
- });
382
-
383
- it('should list available resources', async () => {
384
- const result = (await resourceTestInternalClient.listResources()) as ListResourcesResult;
385
- expect(result).toBeDefined();
386
- expect(result.resources.length).toBe(initialResourcesForTest.length);
387
- initialResourcesForTest.forEach(mockResource => {
388
- expect(result.resources).toContainEqual(expect.objectContaining(mockResource));
389
- });
390
- });
391
-
392
- it('should read content for weather://current', async () => {
393
- const uri = 'weather://current';
394
- const resourceContentResult = (await resourceTestInternalClient.readResource(uri)) as ReadResourceResult;
395
-
396
- expect(resourceContentResult).toBeDefined();
397
- expect(resourceContentResult.contents).toBeDefined();
398
- expect(resourceContentResult.contents.length).toBe(1);
399
-
400
- const content = resourceContentResult.contents[0];
401
- expect(content.uri).toBe(uri);
402
- expect(content.mimeType).toBe('application/json');
403
- expect(content.text).toBe((mockResourceContents[uri] as { text: string }).text);
404
- });
405
-
406
- it('should read content for weather://forecast', async () => {
407
- const uri = 'weather://forecast';
408
- const resourceContentResult = (await resourceTestInternalClient.readResource(uri)) as ReadResourceResult;
409
- expect(resourceContentResult.contents.length).toBe(1);
410
- const content = resourceContentResult.contents[0];
411
- expect(content.uri).toBe(uri);
412
- expect(content.mimeType).toBe('application/json');
413
- expect(content.text).toBe((mockResourceContents[uri] as { text: string }).text);
414
- });
415
-
416
- it('should read content for weather://historical', async () => {
417
- const uri = 'weather://historical';
418
- const resourceContentResult = (await resourceTestInternalClient.readResource(uri)) as ReadResourceResult;
419
- expect(resourceContentResult.contents.length).toBe(1);
420
- const content = resourceContentResult.contents[0];
421
- expect(content.uri).toBe(uri);
422
- expect(content.mimeType).toBe('application/json');
423
- expect(content.text).toBe((mockResourceContents[uri] as { text: string }).text);
424
- });
425
-
426
- it('should throw an error when reading a non-existent resource URI', async () => {
427
- const uri = 'weather://nonexistent';
428
- await expect(resourceTestInternalClient.readResource(uri)).rejects.toThrow(
429
- 'Resource not found: weather://nonexistent',
430
- );
431
- });
432
- });
433
-
434
- describe('MCPServer Resource Handling with Notifications and Templates', () => {
435
- let notificationTestServer: MCPServer;
436
- let notificationTestInternalClient: InternalMastraMCPClient;
437
- let notificationHttpServer: http.Server;
438
- const NOTIFICATION_PORT = 9400 + Math.floor(Math.random() * 1000);
439
-
440
- const mockInitialResources: Resource[] = [
441
- {
442
- uri: 'test://resource/1',
443
- name: 'Resource 1',
444
- mimeType: 'text/plain',
445
- },
446
- {
447
- uri: 'test://resource/2',
448
- name: 'Resource 2',
449
- mimeType: 'application/json',
450
- },
451
- ];
452
-
453
- let mockCurrentResourceContents: Record<string, MCPServerResourceContent> = {
454
- 'test://resource/1': { text: 'Initial content for R1' },
455
- 'test://resource/2': { text: JSON.stringify({ data: 'Initial for R2' }) },
456
- };
457
-
458
- const mockResourceTemplates: ResourceTemplate[] = [
459
- {
460
- uriTemplate: 'test://template/{id}',
461
- name: 'Test Template',
462
- description: 'A template for test resources',
463
- },
464
- ];
465
-
466
- const getResourceContentCallback = vi.fn(async ({ uri }: { uri: string }) => {
467
- if (mockCurrentResourceContents[uri]) {
468
- return mockCurrentResourceContents[uri];
469
- }
470
- throw new Error(`Mock content not found for ${uri}`);
471
- });
472
-
473
- const listResourcesCallback = vi.fn(async () => mockInitialResources);
474
- const resourceTemplatesCallback = vi.fn(async () => mockResourceTemplates);
475
-
476
- beforeAll(async () => {
477
- const serverOptions: MCPServerConfig & { resources?: MCPServerResources } = {
478
- name: 'NotificationTestServer',
479
- version: '1.0.0',
480
- tools: minimalTestTool,
481
- resources: {
482
- listResources: listResourcesCallback,
483
- getResourceContent: getResourceContentCallback,
484
- resourceTemplates: resourceTemplatesCallback,
485
- },
486
- };
487
- notificationTestServer = new MCPServer(serverOptions);
488
-
489
- notificationHttpServer = http.createServer(async (req, res) => {
490
- const url = new URL(req.url || '', `http://localhost:${NOTIFICATION_PORT}`);
491
- await notificationTestServer.startSSE({
492
- url,
493
- ssePath: '/sse',
494
- messagePath: '/message',
495
- req,
496
- res,
497
- });
498
- });
499
- await new Promise<void>(resolve => notificationHttpServer.listen(NOTIFICATION_PORT, resolve));
500
-
501
- notificationTestInternalClient = new InternalMastraMCPClient({
502
- name: 'notification-internal-client',
503
- server: {
504
- url: new URL(`http://localhost:${NOTIFICATION_PORT}/sse`),
505
- logger: logMessage =>
506
- console.log(
507
- `[${logMessage.serverName} - ${logMessage.level.toUpperCase()}]: ${logMessage.message}`,
508
- logMessage.details || '',
509
- ),
510
- },
511
- });
512
- await notificationTestInternalClient.connect();
513
- });
514
-
515
- afterAll(async () => {
516
- await notificationTestInternalClient.disconnect();
517
- if (notificationHttpServer) {
518
- await new Promise<void>((resolve, reject) =>
519
- notificationHttpServer.close(err => {
520
- if (err) return reject(err);
521
- resolve();
522
- }),
523
- );
524
- }
525
- await notificationTestServer.close();
526
- });
527
-
528
- beforeEach(() => {
529
- vi.clearAllMocks();
530
- // Reset resource contents for isolation, though specific tests might override
531
- mockCurrentResourceContents = {
532
- 'test://resource/1': { text: 'Initial content for R1' },
533
- 'test://resource/2': { text: JSON.stringify({ data: 'Initial for R2' }) },
534
- };
535
- });
536
-
537
- it('should list initial resources', async () => {
538
- const result = (await notificationTestInternalClient.listResources()) as ListResourcesResult;
539
- expect(listResourcesCallback).toHaveBeenCalledTimes(1);
540
- expect(result.resources).toEqual(mockInitialResources);
541
- });
542
-
543
- it('should read resource content for an existing resource', async () => {
544
- const uri = 'test://resource/1';
545
- const result = (await notificationTestInternalClient.readResource(uri)) as ReadResourceResult;
546
- expect(getResourceContentCallback).toHaveBeenCalledWith({ uri });
547
- expect(result.contents).toEqual([
548
- {
549
- uri,
550
- mimeType: mockInitialResources.find(r => r.uri === uri)?.mimeType,
551
- text: (mockCurrentResourceContents[uri] as { text: string }).text,
552
- },
553
- ]);
554
- });
555
-
556
- it('should throw an error when reading a non-existent resource', async () => {
557
- const uri = 'test://resource/nonexistent';
558
- await expect(notificationTestInternalClient.readResource(uri)).rejects.toThrow(
559
- 'Resource not found: test://resource/nonexistent',
560
- );
561
- });
562
-
563
- it('should list resource templates', async () => {
564
- const result = (await notificationTestInternalClient.listResourceTemplates()) as ListResourceTemplatesResult;
565
- expect(resourceTemplatesCallback).toHaveBeenCalledTimes(1);
566
- expect(result.resourceTemplates).toEqual(mockResourceTemplates);
567
- });
568
-
569
- it('should subscribe and unsubscribe from a resource', async () => {
570
- const uri = 'test://resource/1';
571
- const subscribeResult = await notificationTestInternalClient.subscribeResource(uri);
572
- expect(subscribeResult).toEqual({});
573
-
574
- const unsubscribeResult = await notificationTestInternalClient.unsubscribeResource(uri);
575
- expect(unsubscribeResult).toEqual({});
576
- });
577
-
578
- it('should receive resource updated notification when subscribed resource changes', async () => {
579
- const uriToSubscribe = 'test://resource/1';
580
- const newContent = 'Updated content for R1';
581
- const resourceUpdatedPromise = new Promise<void>(resolve => {
582
- notificationTestInternalClient.setResourceUpdatedNotificationHandler((params: { uri: string }) => {
583
- if (params.uri === uriToSubscribe) {
584
- resolve();
585
- }
586
- });
587
- });
588
-
589
- await notificationTestInternalClient.subscribeResource(uriToSubscribe);
590
-
591
- mockCurrentResourceContents[uriToSubscribe] = { text: newContent };
592
-
593
- await notificationTestServer.resources.notifyUpdated({ uri: uriToSubscribe });
594
-
595
- await expect(resourceUpdatedPromise).resolves.toBeUndefined(); // Wait for the notification
596
- await notificationTestInternalClient.unsubscribeResource(uriToSubscribe);
597
- });
598
-
599
- it('should receive resource list changed notification', async () => {
600
- const listChangedPromise = new Promise<void>(resolve => {
601
- notificationTestInternalClient.setResourceListChangedNotificationHandler(() => {
602
- resolve();
603
- });
604
- });
605
-
606
- await notificationTestServer.resources.notifyListChanged();
607
-
608
- await expect(listChangedPromise).resolves.toBeUndefined(); // Wait for the notification
609
- });
610
- });
611
-
612
- describe('Prompts', () => {
613
- let promptServer: MCPServer;
614
- let promptInternalClient: InternalMastraMCPClient;
615
- let promptHttpServer: http.Server;
616
- const PROMPT_PORT = 9500 + Math.floor(Math.random() * 1000);
617
-
618
- let currentPrompts: Prompt[] = [
619
- {
620
- name: 'explain-code',
621
- version: 'v1',
622
- description: 'Explain code v1',
623
- arguments: [{ name: 'code', required: true }],
624
- getMessages: async (args: any) => [
625
- { role: 'user', content: { type: 'text', text: `Explain this code (v1):\n${args.code}` } },
626
- ],
627
- },
628
- {
629
- name: 'explain-code',
630
- version: 'v2',
631
- description: 'Explain code v2',
632
- arguments: [{ name: 'code', required: true }],
633
- getMessages: async (args: any) => [
634
- { role: 'user', content: { type: 'text', text: `Explain this code (v2):\n${args.code}` } },
635
- ],
636
- },
637
- {
638
- name: 'summarize',
639
- version: 'v1',
640
- description: 'Summarize text',
641
- arguments: [{ name: 'text', required: true }],
642
- getMessages: async (args: any) => [
643
- { role: 'user', content: { type: 'text', text: `Summarize this:\n${args.text}` } },
644
- ],
645
- },
646
- ];
647
-
648
- beforeAll(async () => {
649
- // Register multiple versions of the same prompt
650
-
651
- promptServer = new MCPServer({
652
- name: 'PromptTestServer',
653
- version: '1.0.0',
654
- tools: {},
655
- prompts: {
656
- listPrompts: async () => currentPrompts,
657
- getPromptMessages: async (params: { name: string; version?: string; args?: any }) => {
658
- let prompt;
659
- if (params.version) {
660
- prompt = currentPrompts.find(p => p.name === params.name && p.version === params.version);
661
- } else {
662
- // Select the first matching name if no version is provided.
663
- prompt = currentPrompts.find(p => p.name === params.name);
664
- }
665
- if (!prompt)
666
- throw new Error(
667
- `Prompt "${params.name}"${params.version ? ` (version ${params.version})` : ''} not found`,
668
- );
669
- return (prompt as any).getMessages(params.args);
670
- },
671
- },
672
- });
673
-
674
- promptHttpServer = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
675
- const url = new URL(req.url || '', `http://localhost:${PROMPT_PORT}`);
676
- await promptServer.startSSE({
677
- url,
678
- ssePath: '/sse',
679
- messagePath: '/messages',
680
- req,
681
- res,
682
- });
683
- });
684
- await new Promise<void>(resolve => promptHttpServer.listen(PROMPT_PORT, () => resolve()));
685
- promptInternalClient = new InternalMastraMCPClient({
686
- name: 'prompt-test-internal-client',
687
- server: { url: new URL(`http://localhost:${PROMPT_PORT}/sse`) },
688
- });
689
- await promptInternalClient.connect();
690
- });
691
-
692
- afterAll(async () => {
693
- await promptInternalClient.disconnect();
694
- if (promptHttpServer) {
695
- promptHttpServer.closeAllConnections?.();
696
- await new Promise<void>((resolve, reject) => {
697
- promptHttpServer.close(err => {
698
- if (err) return reject(err);
699
- resolve();
700
- });
701
- });
702
- }
703
- if (promptServer) {
704
- await promptServer.close();
705
- }
706
- });
707
-
708
- it('should send prompt list changed notification when prompts change', async () => {
709
- const listChangedPromise = new Promise<void>(resolve => {
710
- promptInternalClient.setPromptListChangedNotificationHandler(() => {
711
- resolve();
712
- });
713
- });
714
- await promptServer.prompts.notifyListChanged();
715
-
716
- await expect(listChangedPromise).resolves.toBeUndefined(); // Wait for the notification
717
- });
718
-
719
- it('should list all prompts with version field', async () => {
720
- const result = await promptInternalClient.listPrompts();
721
- expect(result).toBeDefined();
722
- expect(result.prompts).toBeInstanceOf(Array);
723
- // Should contain both explain-code v1 and v2 and summarize v1
724
- const explainV1 = result.prompts.find((p: Prompt) => p.name === 'explain-code' && p.version === 'v1');
725
- const explainV2 = result.prompts.find((p: Prompt) => p.name === 'explain-code' && p.version === 'v2');
726
- const summarizeV1 = result.prompts.find((p: Prompt) => p.name === 'summarize' && p.version === 'v1');
727
- expect(explainV1).toBeDefined();
728
- expect(explainV2).toBeDefined();
729
- expect(summarizeV1).toBeDefined();
730
- });
731
-
732
- it('should retrieve prompt by name and version', async () => {
733
- const result = await promptInternalClient.getPrompt({
734
- name: 'explain-code',
735
- args: { code: 'let x = 1;' },
736
- version: 'v2',
737
- });
738
- const prompt = result.prompt as GetPromptResult;
739
- expect(prompt).toBeDefined();
740
- expect(prompt.name).toBe('explain-code');
741
- expect(prompt.version).toBe('v2');
742
-
743
- const messages = result.messages;
744
- expect(messages).toBeDefined();
745
- expect(messages.length).toBeGreaterThan(0);
746
- expect(messages[0].content.text).toContain('(v2)');
747
- });
748
-
749
- it('should retrieve prompt by name and default to first version if not specified', async () => {
750
- const result = await promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'let y = 2;' } });
751
- expect(result.prompt).toBeDefined();
752
- const prompt = result.prompt as GetPromptResult;
753
- expect(prompt.name).toBe('explain-code');
754
- // Should default to first version (v1)
755
- expect(prompt.version).toBe('v1');
756
-
757
- const messages = result.messages;
758
- expect(messages).toBeDefined();
759
- expect(messages.length).toBeGreaterThan(0);
760
- expect(messages[0].content.text).toContain('(v1)');
761
- });
762
-
763
- it('should return error if prompt name/version does not exist', async () => {
764
- await expect(
765
- promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'foo' }, version: 'v999' }),
766
- ).rejects.toThrow();
767
- });
768
- it('should throw error if required argument is missing', async () => {
769
- await expect(
770
- promptInternalClient.getPrompt({ name: 'explain-code', args: {} }), // missing 'code'
771
- ).rejects.toThrow(/Missing required argument/);
772
- });
773
-
774
- it('should succeed if all required arguments are provided', async () => {
775
- const result = await promptInternalClient.getPrompt({ name: 'explain-code', args: { code: 'let z = 3;' } });
776
- expect(result.prompt).toBeDefined();
777
- expect(result.messages[0].content.text).toContain('let z = 3;');
778
- });
779
- it('should allow prompts with optional arguments', async () => {
780
- // Register a prompt with an optional argument
781
- currentPrompts = [
782
- {
783
- name: 'optional-arg-prompt',
784
- version: 'v1',
785
- description: 'Prompt with optional argument',
786
- arguments: [{ name: 'foo', required: false }],
787
- getMessages: async (args: any) => [
788
- { role: 'user', content: { type: 'text', text: `foo is: ${args.foo ?? 'none'}` } },
789
- ],
790
- },
791
- ];
792
- await promptServer.prompts.notifyListChanged();
793
- const result = await promptInternalClient.getPrompt({ name: 'optional-arg-prompt', args: {} });
794
- expect(result.prompt).toBeDefined();
795
- expect(result.messages[0].content.text).toContain('foo is: none');
796
- });
797
- it('should retrieve prompt with no version field by name only', async () => {
798
- currentPrompts = [
799
- {
800
- name: 'no-version',
801
- description: 'Prompt without version',
802
- arguments: [],
803
- getMessages: async () => [{ role: 'user', content: { type: 'text', text: 'no version' } }],
804
- },
805
- ];
806
- await promptServer.prompts.notifyListChanged();
807
- const result = await promptInternalClient.getPrompt({ name: 'no-version', args: {} });
808
- const prompt = result.prompt as GetPromptResult;
809
- expect(prompt).toBeDefined();
810
- expect(prompt.version).toBeUndefined();
811
- const messages = result.messages;
812
- expect(messages).toBeDefined();
813
- expect(messages.length).toBeGreaterThan(0);
814
- expect(messages[0].content.text).toContain('no version');
815
- });
816
- it('should list prompts with required fields', async () => {
817
- const result = await promptInternalClient.listPrompts();
818
- result.prompts.forEach((p: Prompt) => {
819
- expect(p.name).toBeDefined();
820
- expect(p.description).toBeDefined();
821
- expect(p.arguments).toBeDefined();
822
- });
823
- });
824
- it('should return empty list if no prompts are registered', async () => {
825
- currentPrompts = [];
826
- await promptServer.prompts.notifyListChanged();
827
- const result = await promptInternalClient.listPrompts();
828
- expect(result.prompts).toBeInstanceOf(Array);
829
- expect(result.prompts.length).toBe(0);
830
- });
831
- });
832
-
833
- describe('MCPServer SSE transport', () => {
834
- let sseRes: Response | undefined;
835
- let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
836
-
837
- beforeAll(async () => {
838
- server = new MCPServer({
839
- name: 'Test MCP Server',
840
- version: '0.1.0',
841
- tools: { weatherTool },
842
- });
843
-
844
- httpServer = http.createServer(async (req, res) => {
845
- const url = new URL(req.url || '', `http://localhost:${PORT}`);
846
- await server.startSSE({
847
- url,
848
- ssePath: '/sse',
849
- messagePath: '/message',
850
- req,
851
- res,
852
- });
853
- });
854
-
855
- await new Promise<void>(resolve => httpServer.listen(PORT, () => resolve()));
856
- });
857
-
858
- afterAll(async () => {
859
- await new Promise<void>((resolve, reject) =>
860
- httpServer.close(err => {
861
- if (err) return reject(err);
862
- resolve();
863
- }),
864
- );
865
- });
866
-
867
- afterEach(async () => {
868
- if (reader) {
869
- try {
870
- await reader.cancel();
871
- } catch {}
872
- reader = undefined;
873
- }
874
- if (sseRes && 'body' in sseRes && sseRes.body) {
875
- try {
876
- await sseRes.body.cancel();
877
- } catch {}
878
- sseRes = undefined;
879
- }
880
- });
881
-
882
- it('should parse SSE stream and contain tool output', async () => {
883
- sseRes = await fetch(`http://localhost:${PORT}/sse`, {
884
- headers: { Accept: 'text/event-stream' },
885
- });
886
- expect(sseRes.status).toBe(200);
887
- reader = sseRes.body?.getReader();
888
- expect(reader).toBeDefined();
889
- await fetch(`http://localhost:${PORT}/message`, {
890
- method: 'POST',
891
- headers: { 'Content-Type': 'application/json' },
892
- body: JSON.stringify({ tool: 'weatherTool', input: { location: 'Austin' } }),
893
- });
894
- if (reader) {
895
- const { value } = await reader.read();
896
- const text = value ? new TextDecoder().decode(value) : '';
897
- expect(text).toMatch(/data:/);
898
- }
899
- });
900
-
901
- it('should return 503 if message sent before SSE connection', async () => {
902
- (server as any).sseTransport = undefined;
903
- const res = await fetch(`http://localhost:${PORT}/message`, {
904
- method: 'POST',
905
- headers: { 'Content-Type': 'application/json' },
906
- body: JSON.stringify({ tool: 'weatherTool', input: { location: 'Austin' } }),
907
- });
908
- expect(res.status).toBe(503);
909
- });
910
- });
911
-
912
- describe('MCPServer stdio transport', () => {
913
- it('should connect and expose stdio transport', async () => {
914
- await server.startStdio();
915
- expect(server.getStdioTransport()).toBeInstanceOf(StdioServerTransport);
916
- });
917
- it('should use stdio transport to get tools', async () => {
918
- const existingConfig = new MCPClient({
919
- servers: {
920
- weather: {
921
- command: 'npx',
922
- args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/server-weather.ts')],
923
- env: {
924
- FAKE_CREDS: 'test',
925
- },
926
- },
927
- },
928
- });
929
-
930
- const tools = await existingConfig.getTools();
931
- expect(Object.keys(tools).length).toBeGreaterThan(0);
932
- expect(Object.keys(tools)[0]).toBe('weather_weatherTool');
933
- await existingConfig.disconnect();
934
- });
935
- });
936
- describe('MCPServer HTTP Transport', () => {
937
- let server: MCPServer;
938
- let client: MCPClient;
939
- const PORT = 9200 + Math.floor(Math.random() * 1000);
940
- const TOKEN = `<random-token>`;
941
-
942
- beforeAll(async () => {
943
- server = new MCPServer({
944
- name: 'Test MCP Server',
945
- version: '0.1.0',
946
- tools: {
947
- weatherTool,
948
- testAuthTool: {
949
- description: 'Test tool to validate auth information from extra params',
950
- parameters: z.object({
951
- message: z.string().describe('Message to show to user'),
952
- }),
953
- execute: async (context, options) => {
954
- const extra = options.extra as MCPRequestHandlerExtra;
955
-
956
- return {
957
- message: context.message,
958
- sessionId: extra?.sessionId || null,
959
- authInfo: extra?.authInfo || null,
960
- requestId: extra?.requestId || null,
961
- hasExtra: !!extra,
962
- };
963
- },
964
- },
965
- },
966
- });
967
-
968
- httpServer = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
969
- const url = new URL(req.url || '', `http://localhost:${PORT}`);
970
- await server.startHTTP({
971
- url,
972
- httpPath: '/http',
973
- req,
974
- res,
975
- options: {
976
- sessionIdGenerator: undefined,
977
- },
978
- });
979
- });
980
-
981
- await new Promise<void>(resolve => httpServer.listen(PORT, () => resolve()));
982
-
983
- client = new MCPClient({
984
- servers: {
985
- local: {
986
- url: new URL(`http://localhost:${PORT}/http`),
987
- requestInit: {
988
- headers: { Authorization: `Bearer ${TOKEN}` },
989
- },
990
- },
991
- },
992
- });
993
- });
994
-
995
- afterAll(async () => {
996
- httpServer.closeAllConnections?.();
997
- await new Promise<void>(resolve =>
998
- httpServer.close(() => {
999
- resolve();
1000
- }),
1001
- );
1002
- await server.close();
1003
- });
1004
-
1005
- it('should return 404 for wrong path', async () => {
1006
- const res = await fetch(`http://localhost:${PORT}/wrong`);
1007
- expect(res.status).toBe(404);
1008
- });
1009
-
1010
- it('should respond to HTTP request using client', async () => {
1011
- const tools = await client.getTools();
1012
- const tool = tools['local_weatherTool'];
1013
- expect(tool).toBeDefined();
1014
-
1015
- // Call the tool
1016
- const result = await tool.execute({ context: { location: 'Austin' } });
1017
-
1018
- // Check the result
1019
- expect(result).toBeDefined();
1020
- expect(result.content).toBeInstanceOf(Array);
1021
- expect(result.content.length).toBeGreaterThan(0);
1022
-
1023
- const toolOutput = result.content[0];
1024
- expect(toolOutput.type).toBe('text');
1025
- const toolResult = JSON.parse(toolOutput.text);
1026
- expect(toolResult.location).toEqual('Austin');
1027
- expect(toolResult).toHaveProperty('temperature');
1028
- expect(toolResult).toHaveProperty('feelsLike');
1029
- expect(toolResult).toHaveProperty('humidity');
1030
- expect(toolResult).toHaveProperty('conditions');
1031
- expect(toolResult).toHaveProperty('windSpeed');
1032
- expect(toolResult).toHaveProperty('windGust');
1033
- });
1034
-
1035
- it('should pass auth information through extra parameter', async () => {
1036
- const mockExtra: MCPRequestHandlerExtra = {
1037
- signal: new AbortController().signal,
1038
- sessionId: 'test-session-id',
1039
- authInfo: {
1040
- token: TOKEN,
1041
- clientId: 'test-client-id',
1042
- scopes: ['read'],
1043
- },
1044
- requestId: 'test-request-id',
1045
- sendNotification: vi.fn(),
1046
- sendRequest: vi.fn(),
1047
- };
1048
-
1049
- const mockRequest = {
1050
- jsonrpc: '2.0' as const,
1051
- id: 'test-request-1',
1052
- method: 'tools/call' as const,
1053
- params: {
1054
- name: 'testAuthTool',
1055
- arguments: {
1056
- message: 'test auth',
1057
- },
1058
- },
1059
- };
1060
-
1061
- const serverInstance = server.getServer();
1062
-
1063
- // @ts-ignore - this is a private property, but we need to access it to test the request handler
1064
- const requestHandlers = serverInstance._requestHandlers;
1065
- const callToolHandler = requestHandlers.get('tools/call');
1066
-
1067
- expect(callToolHandler).toBeDefined();
1068
-
1069
- const result = await callToolHandler(mockRequest, mockExtra);
1070
-
1071
- expect(result).toBeDefined();
1072
- expect(result.isError).toBe(false);
1073
- expect(result.content).toBeInstanceOf(Array);
1074
- expect(result.content.length).toBeGreaterThan(0);
1075
-
1076
- const toolOutput = result.content[0];
1077
- expect(toolOutput.type).toBe('text');
1078
- const toolResult = JSON.parse(toolOutput.text);
1079
-
1080
- expect(toolResult.message).toBe('test auth');
1081
- expect(toolResult.hasExtra).toBe(true);
1082
- expect(toolResult.sessionId).toBe('test-session-id');
1083
- expect(toolResult.authInfo).toBeDefined();
1084
- expect(toolResult.authInfo.token).toBe(TOKEN);
1085
- expect(toolResult.authInfo.clientId).toBe('test-client-id');
1086
- expect(toolResult.requestId).toBe('test-request-id');
1087
- });
1088
- });
1089
-
1090
- describe('MCPServer Hono SSE Transport', () => {
1091
- let server: MCPServer;
1092
- let hono: Hono;
1093
- let honoServer: ServerType;
1094
- let client: MCPClient;
1095
- const PORT = 9300 + Math.floor(Math.random() * 1000);
1096
-
1097
- beforeAll(async () => {
1098
- server = new MCPServer({
1099
- name: 'Test MCP Server',
1100
- version: '0.1.0',
1101
- tools: { weatherTool },
1102
- });
1103
-
1104
- hono = new Hono();
1105
-
1106
- hono.get('/sse', async c => {
1107
- const url = new URL(c.req.url, `http://localhost:${PORT}`);
1108
- return await server.startHonoSSE({
1109
- url,
1110
- ssePath: '/sse',
1111
- messagePath: '/message',
1112
- context: c,
1113
- });
1114
- });
1115
-
1116
- hono.post('/message', async c => {
1117
- // Use MCPServer's startHonoSSE to handle message endpoint
1118
- const url = new URL(c.req.url, `http://localhost:${PORT}`);
1119
- return await server.startHonoSSE({
1120
- url,
1121
- ssePath: '/sse',
1122
- messagePath: '/message',
1123
- context: c,
1124
- });
1125
- });
1126
-
1127
- honoServer = serve({ fetch: hono.fetch, port: PORT });
1128
-
1129
- // Initialize MCPClient with SSE endpoint
1130
- client = new MCPClient({
1131
- servers: {
1132
- local: {
1133
- url: new URL(`http://localhost:${PORT}/sse`),
1134
- },
1135
- },
1136
- });
1137
- });
1138
-
1139
- afterAll(async () => {
1140
- honoServer.close();
1141
- await server.close();
1142
- });
1143
-
1144
- it('should respond to SSE connection and tool call', async () => {
1145
- // Get tools from the client
1146
- const tools = await client.getTools();
1147
- const tool = tools['local_weatherTool'];
1148
- expect(tool).toBeDefined();
1149
-
1150
- // Call the tool using the MCPClient (SSE transport)
1151
- const result = await tool.execute({ context: { location: 'Austin' } });
1152
-
1153
- expect(result).toBeDefined();
1154
- expect(result.content).toBeInstanceOf(Array);
1155
- expect(result.content.length).toBeGreaterThan(0);
1156
-
1157
- const toolOutput = result.content[0];
1158
- expect(toolOutput.type).toBe('text');
1159
- const toolResult = JSON.parse(toolOutput.text);
1160
- expect(toolResult.location).toEqual('Austin');
1161
- expect(toolResult).toHaveProperty('temperature');
1162
- expect(toolResult).toHaveProperty('feelsLike');
1163
- expect(toolResult).toHaveProperty('humidity');
1164
- expect(toolResult).toHaveProperty('conditions');
1165
- expect(toolResult).toHaveProperty('windSpeed');
1166
- expect(toolResult).toHaveProperty('windGust');
1167
- });
1168
- });
1169
- });
1170
-
1171
- describe('MCPServer - Agent to Tool Conversion', () => {
1172
- let server: MCPServer;
1173
-
1174
- beforeEach(() => {
1175
- vi.clearAllMocks();
1176
- });
1177
-
1178
- it('should convert a provided agent to an MCP tool with sync dynamic description', () => {
1179
- const testAgent = createMockAgent(
1180
- 'MyTestAgent',
1181
- mockAgentGenerate,
1182
- mockAgentGetInstructions,
1183
- 'Simple mock description.',
1184
- );
1185
- server = new MCPServer({
1186
- name: 'AgentToolServer',
1187
- version: '1.0.0',
1188
- tools: {},
1189
- agents: { testAgentKey: testAgent },
1190
- });
1191
-
1192
- const tools = server.tools();
1193
- const agentToolName = 'ask_testAgentKey';
1194
- expect(tools[agentToolName]).toBeDefined();
1195
- expect(tools[agentToolName].description).toContain("Ask agent 'MyTestAgent' a question.");
1196
- expect(tools[agentToolName].description).toContain('Agent description: Simple mock description.');
1197
-
1198
- const schema = tools[agentToolName].parameters.jsonSchema;
1199
- expect(schema.type).toBe('object');
1200
- if (schema.properties) {
1201
- expect(schema.properties.message).toBeDefined();
1202
- const querySchema = schema.properties.message as any;
1203
- expect(querySchema.type).toBe('string');
1204
- } else {
1205
- throw new Error('Schema properties are undefined'); // Fail test if properties not found
1206
- }
1207
- });
1208
-
1209
- it('should call agent.generate when the derived tool is executed', async () => {
1210
- const testAgent = createMockAgent(
1211
- 'MyExecAgent',
1212
- mockAgentGenerate,
1213
- mockAgentGetInstructions,
1214
- 'Executable mock agent',
1215
- );
1216
- server = new MCPServer({
1217
- name: 'AgentExecServer',
1218
- version: '1.0.0',
1219
- tools: {},
1220
- agents: { execAgentKey: testAgent },
1221
- });
1222
-
1223
- const agentTool = server.tools()['ask_execAgentKey'];
1224
- expect(agentTool).toBeDefined();
1225
-
1226
- const queryInput = { message: 'Hello Agent' };
1227
-
1228
- if (agentTool && agentTool.execute) {
1229
- const result = await agentTool.execute(queryInput, { toolCallId: 'mcp-call-123', messages: [] });
1230
-
1231
- expect(mockAgentGenerate).toHaveBeenCalledTimes(1);
1232
- expect(mockAgentGenerate).toHaveBeenCalledWith(queryInput.message);
1233
- expect(result.text).toBe(`{"content":"Agent response to: ""Hello Agent""}`);
1234
- } else {
1235
- throw new Error('Agent tool or its execute function is undefined');
1236
- }
1237
- });
1238
-
1239
- it('should handle name collision: explicit tool wins over agent-derived tool', () => {
1240
- const explicitToolName = 'ask_collidingAgentKey';
1241
- const explicitToolExecute = vi.fn(async () => 'explicit tool response');
1242
- const collidingAgent = createMockAgent(
1243
- 'CollidingAgent',
1244
- mockAgentGenerate,
1245
- undefined,
1246
- 'Colliding agent description',
1247
- );
1248
-
1249
- server = new MCPServer({
1250
- name: 'CollisionServer',
1251
- version: '1.0.0',
1252
- tools: {
1253
- [explicitToolName]: {
1254
- description: 'An explicit tool that collides.',
1255
- parameters: z.object({ query: z.string() }),
1256
- execute: explicitToolExecute,
1257
- },
1258
- },
1259
- agents: { collidingAgentKey: collidingAgent },
1260
- });
1261
-
1262
- const tools = server.tools();
1263
- expect(tools[explicitToolName]).toBeDefined();
1264
- expect(tools[explicitToolName].description).toBe('An explicit tool that collides.');
1265
- expect(mockAgentGenerate).not.toHaveBeenCalled();
1266
- });
1267
-
1268
- it('should use agentKey for tool name ask_<agentKey>', () => {
1269
- const uniqueKeyAgent = createMockAgent(
1270
- 'AgentNameDoesNotMatterForToolKey',
1271
- mockAgentGenerate,
1272
- undefined,
1273
- 'Agent description',
1274
- );
1275
- server = new MCPServer({
1276
- name: 'UniqueKeyServer',
1277
- version: '1.0.0',
1278
- tools: {},
1279
- agents: { unique_agent_key_123: uniqueKeyAgent },
1280
- });
1281
- expect(server.tools()['ask_unique_agent_key_123']).toBeDefined();
1282
- });
1283
-
1284
- it('should throw an error if description is undefined (not provided to mock)', () => {
1285
- const agentWithNoDesc = createMockAgent('NoDescAgent', mockAgentGenerate, mockAgentGetInstructions, undefined); // getDescription will return ''
1286
-
1287
- expect(
1288
- () =>
1289
- new MCPServer({
1290
- name: 'NoDescProvidedServer',
1291
- version: '1.0.0',
1292
- tools: {},
1293
- agents: { noDescKey: agentWithNoDesc as unknown as Agent }, // Cast for test setup
1294
- }),
1295
- ).toThrow('must have a non-empty description');
1296
- });
1297
- });
1298
-
1299
- describe('MCPServer - Workflow to Tool Conversion', () => {
1300
- let server: MCPServer;
1301
-
1302
- beforeEach(() => {
1303
- vi.clearAllMocks();
1304
- });
1305
-
1306
- it('should convert a provided workflow to an MCP tool', () => {
1307
- const testWorkflow = createMockWorkflow('MyTestWorkflow', 'A test workflow.');
1308
- server = new MCPServer({
1309
- name: 'WorkflowToolServer',
1310
- version: '1.0.0',
1311
- tools: {},
1312
- workflows: { testWorkflowKey: testWorkflow },
1313
- });
1314
-
1315
- const tools = server.tools();
1316
- const workflowToolName = 'run_testWorkflowKey';
1317
- expect(tools[workflowToolName]).toBeDefined();
1318
- expect(tools[workflowToolName].description).toBe(
1319
- "Run workflow 'testWorkflowKey'. Workflow description: A test workflow.",
1320
- );
1321
- expect(tools[workflowToolName].parameters.jsonSchema).toBeDefined();
1322
- expect(tools[workflowToolName].parameters.jsonSchema.type).toBe('object');
1323
- });
1324
-
1325
- it('should throw an error if workflow.description is undefined or empty', () => {
1326
- const testWorkflowNoDesc = createMockWorkflow('MyWorkflowNoDesc', undefined);
1327
- expect(
1328
- () =>
1329
- new MCPServer({
1330
- name: 'WorkflowNoDescServer',
1331
- version: '1.0.0',
1332
- tools: {},
1333
- workflows: { testKeyNoDesc: testWorkflowNoDesc },
1334
- }),
1335
- ).toThrow('must have a non-empty description');
1336
-
1337
- const testWorkflowEmptyDesc = createMockWorkflow('MyWorkflowEmptyDesc', '');
1338
- expect(
1339
- () =>
1340
- new MCPServer({
1341
- name: 'WorkflowEmptyDescServer',
1342
- version: '1.0.0',
1343
- tools: {},
1344
- workflows: { testKeyEmptyDesc: testWorkflowEmptyDesc },
1345
- }),
1346
- ).toThrow('must have a non-empty description');
1347
- });
1348
-
1349
- it('should call workflow.createRun().start() when the derived tool is executed', async () => {
1350
- const testWorkflow = createMockWorkflow('MyExecWorkflow', 'Executable workflow', z.object({ data: z.string() }));
1351
- const step = createStep({
1352
- id: 'my-step',
1353
- description: 'My step description',
1354
- inputSchema: z.object({
1355
- data: z.string(),
1356
- }),
1357
- outputSchema: z.object({
1358
- result: z.string(),
1359
- }),
1360
- execute: async ({ inputData }) => {
1361
- return {
1362
- result: inputData.data,
1363
- };
1364
- },
1365
- });
1366
- testWorkflow.then(step).commit();
1367
- server = new MCPServer({
1368
- name: 'WorkflowExecServer',
1369
- version: '1.0.0',
1370
- tools: {},
1371
- workflows: { execWorkflowKey: testWorkflow },
1372
- });
1373
-
1374
- const workflowTool = server.tools()['run_execWorkflowKey'] as ConvertedTool;
1375
- expect(workflowTool).toBeDefined();
1376
-
1377
- const inputData = { data: 'Hello Workflow' };
1378
- if (workflowTool && workflowTool.execute) {
1379
- const result = await workflowTool.execute(inputData, { toolCallId: 'mcp-wf-call-123', messages: [] });
1380
- expect(result).toMatchObject({
1381
- status: 'success',
1382
- steps: {
1383
- input: { data: 'Hello Workflow' },
1384
- 'my-step': { status: 'success', output: { result: 'Hello Workflow' } },
1385
- },
1386
- result: { result: 'Hello Workflow' },
1387
- });
1388
- } else {
1389
- throw new Error('Workflow tool or its execute function is undefined');
1390
- }
1391
- });
1392
-
1393
- it('should handle name collision: explicit tool wins over workflow-derived tool', () => {
1394
- const explicitToolName = 'run_collidingWorkflowKey';
1395
- const explicitToolExecute = vi.fn(async () => 'explicit tool response');
1396
- const collidingWorkflow = createMockWorkflow('CollidingWorkflow', 'Colliding workflow description');
1397
-
1398
- server = new MCPServer({
1399
- name: 'WFCollisionServer',
1400
- version: '1.0.0',
1401
- tools: {
1402
- [explicitToolName]: {
1403
- description: 'An explicit tool that collides with a workflow.',
1404
- parameters: z.object({ query: z.string() }),
1405
- execute: explicitToolExecute,
1406
- },
1407
- },
1408
- workflows: { collidingWorkflowKey: collidingWorkflow },
1409
- });
1410
-
1411
- const tools = server.tools();
1412
- expect(tools[explicitToolName]).toBeDefined();
1413
- expect(tools[explicitToolName].description).toBe('An explicit tool that collides with a workflow.');
1414
- });
1415
-
1416
- it('should use workflowKey for tool name run_<workflowKey>', () => {
1417
- const uniqueKeyWorkflow = createMockWorkflow('WorkflowNameDoesNotMatter', 'WF description');
1418
- server = new MCPServer({
1419
- name: 'UniqueWFKeyServer',
1420
- version: '1.0.0',
1421
- tools: {},
1422
- workflows: { unique_workflow_key_789: uniqueKeyWorkflow },
1423
- });
1424
- expect(server.tools()['run_unique_workflow_key_789']).toBeDefined();
1425
- });
1426
- });
1427
-
1428
- describe('MCPServer - Elicitation', () => {
1429
- let elicitationServer: MCPServer;
1430
- let elicitationClient: InternalMastraMCPClient;
1431
- let elicitationHttpServer: http.Server;
1432
- const ELICITATION_PORT = 9600 + Math.floor(Math.random() * 1000);
1433
-
1434
- beforeAll(async () => {
1435
- elicitationServer = new MCPServer({
1436
- name: 'ElicitationTestServer',
1437
- version: '1.0.0',
1438
- tools: {
1439
- testElicitationTool: {
1440
- description: 'A tool that uses elicitation to collect user input',
1441
- parameters: z.object({
1442
- message: z.string().describe('Message to show to user'),
1443
- }),
1444
- execute: async (context, options) => {
1445
- // Use the session-aware elicitation functionality
1446
- try {
1447
- const elicitation = options.elicitation;
1448
- const result = await elicitation.sendRequest({
1449
- message: context.message,
1450
- requestedSchema: {
1451
- type: 'object',
1452
- properties: {
1453
- name: { type: 'string', title: 'Name' },
1454
- email: { type: 'string', title: 'Email', format: 'email' },
1455
- },
1456
- required: ['name'],
1457
- },
1458
- });
1459
- return result;
1460
- } catch (error) {
1461
- console.error('Error sending elicitation request:', error);
1462
- return {
1463
- content: [
1464
- {
1465
- type: 'text',
1466
- text: `Error collecting information: ${error}`,
1467
- },
1468
- ],
1469
- isError: true,
1470
- };
1471
- }
1472
- },
1473
- },
1474
- },
1475
- });
1476
-
1477
- beforeEach(async () => {
1478
- try {
1479
- await elicitationClient?.disconnect();
1480
- } catch (error) {
1481
- console.error('Error disconnecting elicitation client:', error);
1482
- }
1483
- });
1484
-
1485
- elicitationHttpServer = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
1486
- const url = new URL(req.url || '', `http://localhost:${ELICITATION_PORT}`);
1487
- await elicitationServer.startHTTP({
1488
- url,
1489
- httpPath: '/http',
1490
- req,
1491
- res,
1492
- });
1493
- });
1494
-
1495
- await new Promise<void>(resolve => elicitationHttpServer.listen(ELICITATION_PORT, () => resolve()));
1496
- });
1497
-
1498
- afterAll(async () => {
1499
- await elicitationClient?.disconnect();
1500
- if (elicitationHttpServer) {
1501
- elicitationHttpServer.closeAllConnections?.();
1502
- await new Promise<void>((resolve, reject) => {
1503
- elicitationHttpServer.close(err => {
1504
- if (err) return reject(err);
1505
- resolve();
1506
- });
1507
- });
1508
- }
1509
- if (elicitationServer) {
1510
- await elicitationServer.close();
1511
- }
1512
- });
1513
-
1514
- it('should have elicitation capability enabled', () => {
1515
- // Test that the server has elicitation functionality available
1516
- expect(elicitationServer.elicitation).toBeDefined();
1517
- expect(elicitationServer.elicitation.sendRequest).toBeDefined();
1518
- });
1519
-
1520
- it('should handle elicitation request with accept response', async () => {
1521
- const mockElicitationHandler = vi.fn(async request => {
1522
- expect(request.message).toBe('Please provide your information');
1523
- expect(request.requestedSchema).toBeDefined();
1524
- expect(request.requestedSchema.properties.name).toBeDefined();
1525
-
1526
- return {
1527
- action: 'accept' as const,
1528
- content: {
1529
- name: 'John Doe',
1530
- email: 'john@example.com',
1531
- },
1532
- };
1533
- });
1534
-
1535
- elicitationClient = new InternalMastraMCPClient({
1536
- name: 'elicitation-test-client',
1537
- server: {
1538
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1539
- },
1540
- });
1541
- elicitationClient.elicitation.onRequest(mockElicitationHandler);
1542
- await elicitationClient.connect();
1543
-
1544
- const tools = await elicitationClient.tools();
1545
- const tool = tools['testElicitationTool'];
1546
- expect(tool).toBeDefined();
1547
-
1548
- const result = await tool.execute({
1549
- context: {
1550
- message: 'Please provide your information',
1551
- },
1552
- });
1553
-
1554
- expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1555
- expect(JSON.parse(result.content[0].text)).toEqual({
1556
- action: 'accept',
1557
- content: {
1558
- name: 'John Doe',
1559
- email: 'john@example.com',
1560
- },
1561
- });
1562
- });
1563
-
1564
- it('should handle elicitation request with reject response', async () => {
1565
- const mockElicitationHandler = vi.fn(async request => {
1566
- expect(request.message).toBe('Please provide sensitive data');
1567
- return { action: 'decline' as const };
1568
- });
1569
-
1570
- elicitationClient = new InternalMastraMCPClient({
1571
- name: 'elicitation-reject-client',
1572
- server: {
1573
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1574
- },
1575
- });
1576
- elicitationClient.elicitation.onRequest(mockElicitationHandler);
1577
- await elicitationClient.connect();
1578
-
1579
- const tools = await elicitationClient.tools();
1580
- const tool = tools['testElicitationTool'];
1581
-
1582
- const result = await tool.execute({
1583
- context: {
1584
- message: 'Please provide sensitive data',
1585
- },
1586
- });
1587
-
1588
- expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1589
- expect(JSON.parse(result.content[0].text)).toEqual({ action: 'decline' });
1590
- });
1591
-
1592
- it('should handle elicitation request with cancel response', async () => {
1593
- const mockElicitationHandler = vi.fn(async () => {
1594
- return { action: 'cancel' as const };
1595
- });
1596
-
1597
- elicitationClient = new InternalMastraMCPClient({
1598
- name: 'elicitation-cancel-client',
1599
- server: {
1600
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1601
- },
1602
- });
1603
- elicitationClient.elicitation.onRequest(mockElicitationHandler);
1604
- await elicitationClient.connect();
1605
-
1606
- const tools = await elicitationClient.tools();
1607
- const tool = tools['testElicitationTool'];
1608
-
1609
- const result = await tool.execute({
1610
- context: {
1611
- message: 'Please provide optional data',
1612
- },
1613
- });
1614
-
1615
- expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1616
- expect(JSON.parse(result.content[0].text)).toEqual({ action: 'cancel' });
1617
- });
1618
-
1619
- it('should error when elicitation handler throws error', async () => {
1620
- const mockElicitationHandler = vi.fn(async () => {
1621
- throw new Error('Handler error');
1622
- });
1623
-
1624
- elicitationClient = new InternalMastraMCPClient({
1625
- name: 'elicitation-error-client',
1626
- server: {
1627
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1628
- },
1629
- });
1630
- elicitationClient.elicitation.onRequest(mockElicitationHandler);
1631
- await elicitationClient.connect();
1632
-
1633
- const tools = await elicitationClient.tools();
1634
- const tool = tools['testElicitationTool'];
1635
-
1636
- const result = await tool.execute({
1637
- context: {
1638
- message: 'This will cause an error',
1639
- },
1640
- });
1641
-
1642
- expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1643
- expect(result.content[0].text).toContain('Handler error');
1644
- });
1645
-
1646
- it('should error when client has no elicitation handler', async () => {
1647
- elicitationClient = new InternalMastraMCPClient({
1648
- name: 'no-elicitation-handler-client',
1649
- server: {
1650
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1651
- // No elicitationHandler provided
1652
- },
1653
- });
1654
- await elicitationClient.connect();
1655
-
1656
- const tools = await elicitationClient.tools();
1657
- const tool = tools['testElicitationTool'];
1658
-
1659
- const result = await tool.execute({
1660
- context: {
1661
- message: 'This should fail gracefully',
1662
- },
1663
- });
1664
-
1665
- // When no elicitation handler is provided, the server's elicitInput should fail
1666
- // and the tool should return a reject response
1667
- expect(result.content[0].text).toContain('Method not found');
1668
- });
1669
-
1670
- it('should validate elicitation request schema structure', async () => {
1671
- const mockElicitationHandler = vi.fn(async request => {
1672
- expect(request.message).toBe('Please provide your information');
1673
- expect(request.requestedSchema).toBeDefined();
1674
- expect(request.requestedSchema.properties.name).toBeDefined();
1675
-
1676
- return {
1677
- action: 'accept' as const,
1678
- content: {
1679
- validated: true,
1680
- },
1681
- };
1682
- });
1683
-
1684
- elicitationClient = new InternalMastraMCPClient({
1685
- name: 'elicitation-test-client',
1686
- server: {
1687
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1688
- },
1689
- });
1690
- elicitationClient.elicitation.onRequest(mockElicitationHandler);
1691
- await elicitationClient.connect();
1692
-
1693
- const tools = await elicitationClient.tools();
1694
- const tool = tools['testElicitationTool'];
1695
- expect(tool).toBeDefined();
1696
-
1697
- const result = await tool.execute({
1698
- context: {
1699
- message: 'Please provide your information',
1700
- },
1701
- });
1702
-
1703
- expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1704
- expect(result.content[0].text).toContain('Elicitation response content does not match requested schema');
1705
- });
1706
-
1707
- it('should isolate elicitation handlers between different client connections', async () => {
1708
- const client1Handler = vi.fn(async request => {
1709
- expect(request.message).toBe('Please provide your information');
1710
- expect(request.requestedSchema).toBeDefined();
1711
- expect(request.requestedSchema.properties.name).toBeDefined();
1712
-
1713
- return {
1714
- action: 'accept' as const,
1715
- content: {
1716
- name: 'John Doe',
1717
- email: 'john@example.com',
1718
- },
1719
- };
1720
- });
1721
- const client2Handler = vi.fn(async request => {
1722
- expect(request.message).toBe('Please provide your information');
1723
- expect(request.requestedSchema).toBeDefined();
1724
- expect(request.requestedSchema.properties.name).toBeDefined();
1725
-
1726
- return {
1727
- action: 'accept' as const,
1728
- content: {
1729
- name: 'John Doe',
1730
- email: 'john@example.com',
1731
- },
1732
- };
1733
- });
1734
-
1735
- // Create two independent client instances
1736
- const elicitationClient1 = new MCPClient({
1737
- id: 'elicitation-isolation-client-1',
1738
- servers: {
1739
- elicitation1: {
1740
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1741
- },
1742
- },
1743
- });
1744
-
1745
- const elicitationClient2 = new MCPClient({
1746
- id: 'elicitation-isolation-client-2',
1747
- servers: {
1748
- elicitation2: {
1749
- url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1750
- },
1751
- },
1752
- });
1753
-
1754
- // Each client registers its own independent handler
1755
- elicitationClient1.elicitation.onRequest('elicitation1', client1Handler);
1756
- elicitationClient2.elicitation.onRequest('elicitation2', client2Handler);
1757
-
1758
- const tools = await elicitationClient1.getTools();
1759
- const tool = tools['elicitation1_testElicitationTool'];
1760
- expect(tool).toBeDefined();
1761
- await tool.execute({
1762
- context: {
1763
- message: 'Please provide your information',
1764
- },
1765
- });
1766
-
1767
- const tools2 = await elicitationClient2.getTools();
1768
- const tool2 = tools2['elicitation2_testElicitationTool'];
1769
- expect(tool2).toBeDefined();
1770
-
1771
- // Verify handlers are isolated - they should not interfere with each other
1772
- expect(client1Handler).toHaveBeenCalled();
1773
- expect(client2Handler).not.toHaveBeenCalled();
1774
- }, 10000);
1775
- });
1776
-
1777
- describe('MCPServer with Tool Output Schema', () => {
1778
- let serverWithOutputSchema: MCPServer;
1779
- let clientWithOutputSchema: MCPClient;
1780
- const PORT = 9600 + Math.floor(Math.random() * 1000);
1781
- let httpServerWithOutputSchema: http.Server;
1782
-
1783
- const structuredTool: ToolsInput = {
1784
- structuredTool: {
1785
- description: 'A test tool with structured output',
1786
- parameters: z.object({ input: z.string() }),
1787
- outputSchema: z.object({
1788
- processedInput: z.string(),
1789
- timestamp: z.string(),
1790
- }),
1791
- execute: async ({ input }: { input: string }) => ({
1792
- processedInput: `processed: ${input}`,
1793
- timestamp: mockDateISO,
1794
- }),
1795
- },
1796
- };
1797
-
1798
- beforeAll(async () => {
1799
- serverWithOutputSchema = new MCPServer({
1800
- name: 'Test MCP Server with OutputSchema',
1801
- version: '0.1.0',
1802
- tools: structuredTool,
1803
- });
1804
-
1805
- httpServerWithOutputSchema = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
1806
- const url = new URL(req.url || '', `http://localhost:${PORT}`);
1807
- await serverWithOutputSchema.startHTTP({
1808
- url,
1809
- httpPath: '/http',
1810
- req,
1811
- res,
1812
- });
1813
- });
1814
-
1815
- await new Promise<void>(resolve => httpServerWithOutputSchema.listen(PORT, () => resolve()));
1816
-
1817
- clientWithOutputSchema = new MCPClient({
1818
- servers: {
1819
- local: {
1820
- url: new URL(`http://localhost:${PORT}/http`),
1821
- },
1822
- },
1823
- });
1824
- });
1825
-
1826
- afterAll(async () => {
1827
- httpServerWithOutputSchema.closeAllConnections?.();
1828
- await new Promise<void>(resolve =>
1829
- httpServerWithOutputSchema.close(() => {
1830
- resolve();
1831
- }),
1832
- );
1833
- await serverWithOutputSchema.close();
1834
- });
1835
-
1836
- it('should list tool with outputSchema', async () => {
1837
- const tools = await clientWithOutputSchema.getTools();
1838
- const tool = tools['local_structuredTool'];
1839
- expect(tool).toBeDefined();
1840
- expect(tool.outputSchema).toBeDefined();
1841
- });
1842
-
1843
- it('should call tool and receive structuredContent', async () => {
1844
- const tools = await clientWithOutputSchema.getTools();
1845
- const tool = tools['local_structuredTool'];
1846
- const result = await tool.execute({ context: { input: 'hello' } });
1847
-
1848
- expect(result).toBeDefined();
1849
- expect(result.structuredContent).toBeDefined();
1850
- expect(result.structuredContent.processedInput).toBe('processed: hello');
1851
- expect(result.structuredContent.timestamp).toBe(mockDateISO);
1852
-
1853
- expect(result.content).toBeDefined();
1854
- expect(result.content[0].type).toBe('text');
1855
- expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent);
1856
- });
1857
- });
1858
-
1859
- describe('MCPServer - Tool Input Validation', () => {
1860
- let validationServer: MCPServer;
1861
- let validationClient: InternalMastraMCPClient;
1862
- let httpValidationServer: ServerType;
1863
- let tools: Record<string, any>;
1864
- const VALIDATION_PORT = 9700 + Math.floor(Math.random() * 100);
1865
-
1866
- const toolsWithValidation: ToolsInput = {
1867
- stringTool: {
1868
- description: 'Tool that requires a string input',
1869
- parameters: z.object({
1870
- message: z.string().min(3, 'Message must be at least 3 characters'),
1871
- optional: z.string().optional(),
1872
- }),
1873
- execute: async args => ({
1874
- result: `Received: ${args.message}`,
1875
- }),
1876
- },
1877
- numberTool: {
1878
- description: 'Tool that requires number inputs',
1879
- parameters: z.object({
1880
- age: z.number().min(0).max(150),
1881
- score: z.number().optional(),
1882
- }),
1883
- execute: async args => ({
1884
- result: `Age: ${args.age}, Score: ${args.score ?? 'N/A'}`,
1885
- }),
1886
- },
1887
- complexTool: {
1888
- description: 'Tool with complex validation',
1889
- parameters: z.object({
1890
- email: z.string().email('Invalid email format'),
1891
- tags: z.array(z.string()).min(1, 'At least one tag required'),
1892
- metadata: z.object({
1893
- priority: z.enum(['low', 'medium', 'high']),
1894
- deadline: z.string().datetime().optional(),
1895
- }),
1896
- }),
1897
- execute: async args => ({
1898
- result: `Processing ${args.email} with ${args.tags.length} tags`,
1899
- }),
1900
- },
1901
- };
1902
-
1903
- beforeAll(async () => {
1904
- const app = new Hono();
1905
- validationServer = new MCPServer({
1906
- name: 'ValidationTestServer',
1907
- version: '1.0.0',
1908
- description: 'Server for testing tool validation',
1909
- tools: toolsWithValidation,
1910
- });
1911
-
1912
- app.get('/sse', async c => {
1913
- const url = new URL(c.req.url, `http://localhost:${VALIDATION_PORT}`);
1914
- return await validationServer.startHonoSSE({
1915
- url,
1916
- ssePath: '/sse',
1917
- messagePath: '/message',
1918
- context: c,
1919
- });
1920
- });
1921
-
1922
- app.post('/message', async c => {
1923
- const url = new URL(c.req.url, `http://localhost:${VALIDATION_PORT}`);
1924
- return await validationServer.startHonoSSE({
1925
- url,
1926
- ssePath: '/sse',
1927
- messagePath: '/message',
1928
- context: c,
1929
- });
1930
- });
1931
-
1932
- httpValidationServer = serve({
1933
- fetch: app.fetch,
1934
- port: VALIDATION_PORT,
1935
- });
1936
-
1937
- validationClient = new InternalMastraMCPClient({
1938
- name: 'validation-test-client',
1939
- server: { url: new URL(`http://localhost:${VALIDATION_PORT}/sse`) },
1940
- });
1941
-
1942
- await validationClient.connect();
1943
- tools = await validationClient.tools();
1944
- });
1945
-
1946
- afterAll(async () => {
1947
- await validationClient.disconnect();
1948
- httpValidationServer.close();
1949
- });
1950
-
1951
- it('should successfully execute tool with valid inputs', async () => {
1952
- const stringTool = tools['stringTool'];
1953
- expect(stringTool).toBeDefined();
1954
-
1955
- const result = await stringTool.execute({
1956
- context: {
1957
- message: 'Hello world',
1958
- optional: 'optional value',
1959
- },
1960
- });
1961
-
1962
- expect(result).toBeDefined();
1963
- expect(result.content[0].text).toContain('Received: Hello world');
1964
- });
1965
-
1966
- it('should return validation error for missing required parameters', async () => {
1967
- const stringTool = tools['stringTool'];
1968
- const result = await stringTool.execute({
1969
- context: {},
1970
- });
1971
-
1972
- expect(result).toBeDefined();
1973
- // Handle both client-side and server-side error formats
1974
- if (result.error) {
1975
- expect(result.error).toBe(true);
1976
- expect(result.message).toContain('Tool validation failed');
1977
- expect(result.message).toContain('Please fix the following errors');
1978
- } else {
1979
- expect(result.isError).toBe(true);
1980
- expect(result.content[0].text).toContain('Tool validation failed');
1981
- expect(result.content[0].text).toContain('Please fix the following errors');
1982
- }
1983
- });
1984
-
1985
- it('should return validation error for invalid string length', async () => {
1986
- const stringTool = tools['stringTool'];
1987
- const result = await stringTool.execute({
1988
- context: {
1989
- message: 'Hi', // Too short, min is 3
1990
- },
1991
- });
1992
-
1993
- expect(result).toBeDefined();
1994
- // Handle both client-side and server-side error formats
1995
- if (result.error) {
1996
- expect(result.error).toBe(true);
1997
- expect(result.message).toContain('Tool validation failed');
1998
- expect(result.message).toContain('String must contain at least 3 character(s)');
1999
- } else {
2000
- expect(result.isError).toBe(true);
2001
- expect(result.content[0].text).toContain('Tool validation failed');
2002
- expect(result.content[0].text).toContain('Message must be at least 3 characters');
2003
- }
2004
- });
2005
-
2006
- it('should return validation error for invalid number range', async () => {
2007
- const numberTool = tools['numberTool'];
2008
- const result = await numberTool.execute({
2009
- context: {
2010
- age: -5, // Negative age not allowed
2011
- },
2012
- });
2013
-
2014
- expect(result).toBeDefined();
2015
- // Handle both client-side and server-side error formats
2016
- if (result.error) {
2017
- expect(result.error).toBe(true);
2018
- expect(result.message).toContain('Tool validation failed');
2019
- } else {
2020
- expect(result.isError).toBe(true);
2021
- expect(result.content[0].text).toContain('Tool validation failed');
2022
- }
2023
- });
2024
-
2025
- it('should return validation error for invalid email format', async () => {
2026
- const complexTool = tools['complexTool'];
2027
- const result = await complexTool.execute({
2028
- context: {
2029
- email: 'not-an-email',
2030
- tags: ['tag1'],
2031
- metadata: {
2032
- priority: 'medium',
2033
- },
2034
- },
2035
- });
2036
-
2037
- expect(result).toBeDefined();
2038
- expect(result.isError).toBe(true);
2039
- expect(result.content[0].text).toContain('Tool validation failed');
2040
- expect(result.content[0].text).toContain('Invalid email format');
2041
- });
2042
-
2043
- it('should return validation error for empty array when minimum required', async () => {
2044
- const complexTool = tools['complexTool'];
2045
- const result = await complexTool.execute({
2046
- context: {
2047
- email: 'test@example.com',
2048
- tags: [], // Empty array, min 1 required
2049
- metadata: {
2050
- priority: 'low',
2051
- },
2052
- },
2053
- });
2054
-
2055
- expect(result).toBeDefined();
2056
- // Handle both client-side and server-side error formats
2057
- if (result.error) {
2058
- expect(result.error).toBe(true);
2059
- expect(result.message).toContain('Tool validation failed');
2060
- expect(result.message).toContain('Array must contain at least 1 element(s)');
2061
- } else {
2062
- expect(result.isError).toBe(true);
2063
- expect(result.content[0].text).toContain('Tool validation failed');
2064
- expect(result.content[0].text).toContain('Array must contain at least 1 element(s)');
2065
- }
2066
- });
2067
-
2068
- it('should return validation error for invalid enum value', async () => {
2069
- const complexTool = tools['complexTool'];
2070
- const result = await complexTool.execute({
2071
- context: {
2072
- email: 'test@example.com',
2073
- tags: ['tag1'],
2074
- metadata: {
2075
- priority: 'urgent', // Not in enum ['low', 'medium', 'high']
2076
- },
2077
- },
2078
- });
2079
-
2080
- expect(result).toBeDefined();
2081
- // Handle both client-side and server-side error formats
2082
- if (result.error) {
2083
- expect(result.error).toBe(true);
2084
- expect(result.message).toContain('Tool validation failed');
2085
- } else {
2086
- expect(result.isError).toBe(true);
2087
- expect(result.content[0].text).toContain('Tool validation failed');
2088
- }
2089
- });
2090
-
2091
- it('should handle multiple validation errors', async () => {
2092
- const complexTool = tools['complexTool'];
2093
- const result = await complexTool.execute({
2094
- context: {
2095
- email: 'invalid-email',
2096
- tags: [],
2097
- metadata: {
2098
- priority: 'invalid',
2099
- },
2100
- },
2101
- });
2102
-
2103
- expect(result).toBeDefined();
2104
- // Handle both client-side and server-side error formats
2105
- if (result.error) {
2106
- expect(result.error).toBe(true);
2107
- const errorText = result.message;
2108
- expect(errorText).toContain('Tool validation failed');
2109
- // Should contain multiple validation errors
2110
- // Note: Some validations might not trigger when there are other errors
2111
- expect(errorText).toContain('- tags: Array must contain at least 1 element(s)');
2112
- expect(errorText).toContain('Provided arguments:');
2113
- } else {
2114
- expect(result.isError).toBe(true);
2115
- const errorText = result.content[0].text;
2116
- expect(errorText).toContain('Tool validation failed');
2117
- // Should contain multiple validation errors
2118
- // Note: Some validations might not trigger when there are other errors
2119
- expect(errorText).toContain('- tags: Array must contain at least 1 element(s)');
2120
- expect(errorText).toContain('Provided arguments:');
2121
- }
2122
- });
2123
-
2124
- it('should work with executeTool method directly', async () => {
2125
- // Test valid input
2126
- const validResult = await validationServer.executeTool('stringTool', {
2127
- message: 'Valid message',
2128
- });
2129
- // executeTool returns result directly, not in MCP format
2130
- expect(validResult.result).toBe('Received: Valid message');
2131
-
2132
- // Test invalid input - should return validation error (not throw)
2133
- const invalidResult = await validationServer.executeTool('stringTool', {
2134
- message: 'No', // Too short
2135
- });
2136
-
2137
- // executeTool returns client-side validation format
2138
- expect(invalidResult.error).toBe(true);
2139
- expect(invalidResult.message).toContain('Tool validation failed');
2140
- expect(invalidResult.message).toContain('Message must be at least 3 characters');
2141
- });
2142
- });