@mastra/mcp 0.10.5-alpha.0 → 0.10.5-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.
@@ -1347,3 +1347,436 @@ describe('MCPServer - Workflow to Tool Conversion', () => {
1347
1347
  expect(server.tools()['run_unique_workflow_key_789']).toBeDefined();
1348
1348
  });
1349
1349
  });
1350
+
1351
+ describe('MCPServer - Elicitation', () => {
1352
+ let elicitationServer: MCPServer;
1353
+ let elicitationClient: InternalMastraMCPClient;
1354
+ let elicitationHttpServer: http.Server;
1355
+ const ELICITATION_PORT = 9600 + Math.floor(Math.random() * 1000);
1356
+
1357
+ beforeAll(async () => {
1358
+ elicitationServer = new MCPServer({
1359
+ name: 'ElicitationTestServer',
1360
+ version: '1.0.0',
1361
+ tools: {
1362
+ testElicitationTool: {
1363
+ description: 'A tool that uses elicitation to collect user input',
1364
+ parameters: z.object({
1365
+ message: z.string().describe('Message to show to user'),
1366
+ }),
1367
+ execute: async (context, options) => {
1368
+ // Use the session-aware elicitation functionality
1369
+ try {
1370
+ const elicitation = options.elicitation;
1371
+ const result = await elicitation.sendRequest({
1372
+ message: context.message,
1373
+ requestedSchema: {
1374
+ type: 'object',
1375
+ properties: {
1376
+ name: { type: 'string', title: 'Name' },
1377
+ email: { type: 'string', title: 'Email', format: 'email' },
1378
+ },
1379
+ required: ['name'],
1380
+ },
1381
+ });
1382
+ return result;
1383
+ } catch (error) {
1384
+ console.error('Error sending elicitation request:', error);
1385
+ return {
1386
+ content: [
1387
+ {
1388
+ type: 'text',
1389
+ text: `Error collecting information: ${error}`,
1390
+ },
1391
+ ],
1392
+ isError: true,
1393
+ };
1394
+ }
1395
+ },
1396
+ },
1397
+ },
1398
+ });
1399
+
1400
+ beforeEach(async () => {
1401
+ try {
1402
+ await elicitationClient?.disconnect();
1403
+ } catch (error) {
1404
+ console.error('Error disconnecting elicitation client:', error);
1405
+ }
1406
+ });
1407
+
1408
+ elicitationHttpServer = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
1409
+ const url = new URL(req.url || '', `http://localhost:${ELICITATION_PORT}`);
1410
+ await elicitationServer.startHTTP({
1411
+ url,
1412
+ httpPath: '/http',
1413
+ req,
1414
+ res,
1415
+ });
1416
+ });
1417
+
1418
+ await new Promise<void>(resolve => elicitationHttpServer.listen(ELICITATION_PORT, () => resolve()));
1419
+ });
1420
+
1421
+ afterAll(async () => {
1422
+ await elicitationClient?.disconnect();
1423
+ if (elicitationHttpServer) {
1424
+ elicitationHttpServer.closeAllConnections?.();
1425
+ await new Promise<void>((resolve, reject) => {
1426
+ elicitationHttpServer.close(err => {
1427
+ if (err) return reject(err);
1428
+ resolve();
1429
+ });
1430
+ });
1431
+ }
1432
+ if (elicitationServer) {
1433
+ await elicitationServer.close();
1434
+ }
1435
+ });
1436
+
1437
+ it('should have elicitation capability enabled', () => {
1438
+ // Test that the server has elicitation functionality available
1439
+ expect(elicitationServer.elicitation).toBeDefined();
1440
+ expect(elicitationServer.elicitation.sendRequest).toBeDefined();
1441
+ });
1442
+
1443
+ it('should handle elicitation request with accept response', async () => {
1444
+ const mockElicitationHandler = vi.fn(async request => {
1445
+ expect(request.message).toBe('Please provide your information');
1446
+ expect(request.requestedSchema).toBeDefined();
1447
+ expect(request.requestedSchema.properties.name).toBeDefined();
1448
+
1449
+ return {
1450
+ action: 'accept' as const,
1451
+ content: {
1452
+ name: 'John Doe',
1453
+ email: 'john@example.com',
1454
+ },
1455
+ };
1456
+ });
1457
+
1458
+ elicitationClient = new InternalMastraMCPClient({
1459
+ name: 'elicitation-test-client',
1460
+ server: {
1461
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1462
+ },
1463
+ });
1464
+ elicitationClient.elicitation.onRequest(mockElicitationHandler);
1465
+ await elicitationClient.connect();
1466
+
1467
+ const tools = await elicitationClient.tools();
1468
+ const tool = tools['testElicitationTool'];
1469
+ expect(tool).toBeDefined();
1470
+
1471
+ const result = await tool.execute({
1472
+ context: {
1473
+ message: 'Please provide your information',
1474
+ },
1475
+ });
1476
+
1477
+ expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1478
+ expect(JSON.parse(result.content[0].text)).toEqual({
1479
+ action: 'accept',
1480
+ content: {
1481
+ name: 'John Doe',
1482
+ email: 'john@example.com',
1483
+ },
1484
+ });
1485
+ });
1486
+
1487
+ it('should handle elicitation request with reject response', async () => {
1488
+ const mockElicitationHandler = vi.fn(async request => {
1489
+ expect(request.message).toBe('Please provide sensitive data');
1490
+ return { action: 'reject' as const };
1491
+ });
1492
+
1493
+ elicitationClient = new InternalMastraMCPClient({
1494
+ name: 'elicitation-reject-client',
1495
+ server: {
1496
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1497
+ },
1498
+ });
1499
+ elicitationClient.elicitation.onRequest(mockElicitationHandler);
1500
+ await elicitationClient.connect();
1501
+
1502
+ const tools = await elicitationClient.tools();
1503
+ const tool = tools['testElicitationTool'];
1504
+
1505
+ const result = await tool.execute({
1506
+ context: {
1507
+ message: 'Please provide sensitive data',
1508
+ },
1509
+ });
1510
+
1511
+ expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1512
+ expect(JSON.parse(result.content[0].text)).toEqual({ action: 'reject' });
1513
+ });
1514
+
1515
+ it('should handle elicitation request with cancel response', async () => {
1516
+ const mockElicitationHandler = vi.fn(async () => {
1517
+ return { action: 'cancel' as const };
1518
+ });
1519
+
1520
+ elicitationClient = new InternalMastraMCPClient({
1521
+ name: 'elicitation-cancel-client',
1522
+ server: {
1523
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1524
+ },
1525
+ });
1526
+ elicitationClient.elicitation.onRequest(mockElicitationHandler);
1527
+ await elicitationClient.connect();
1528
+
1529
+ const tools = await elicitationClient.tools();
1530
+ const tool = tools['testElicitationTool'];
1531
+
1532
+ const result = await tool.execute({
1533
+ context: {
1534
+ message: 'Please provide optional data',
1535
+ },
1536
+ });
1537
+
1538
+ expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1539
+ expect(JSON.parse(result.content[0].text)).toEqual({ action: 'cancel' });
1540
+ });
1541
+
1542
+ it('should error when elicitation handler throws error', async () => {
1543
+ const mockElicitationHandler = vi.fn(async () => {
1544
+ throw new Error('Handler error');
1545
+ });
1546
+
1547
+ elicitationClient = new InternalMastraMCPClient({
1548
+ name: 'elicitation-error-client',
1549
+ server: {
1550
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1551
+ },
1552
+ });
1553
+ elicitationClient.elicitation.onRequest(mockElicitationHandler);
1554
+ await elicitationClient.connect();
1555
+
1556
+ const tools = await elicitationClient.tools();
1557
+ const tool = tools['testElicitationTool'];
1558
+
1559
+ const result = await tool.execute({
1560
+ context: {
1561
+ message: 'This will cause an error',
1562
+ },
1563
+ });
1564
+
1565
+ expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1566
+ expect(result.content[0].text).toContain('Handler error');
1567
+ });
1568
+
1569
+ it('should error when client has no elicitation handler', async () => {
1570
+ elicitationClient = new InternalMastraMCPClient({
1571
+ name: 'no-elicitation-handler-client',
1572
+ server: {
1573
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1574
+ // No elicitationHandler provided
1575
+ },
1576
+ });
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: 'This should fail gracefully',
1585
+ },
1586
+ });
1587
+
1588
+ // When no elicitation handler is provided, the server's elicitInput should fail
1589
+ // and the tool should return a reject response
1590
+ expect(result.content[0].text).toContain('Method not found');
1591
+ });
1592
+
1593
+ it('should validate elicitation request schema structure', async () => {
1594
+ const mockElicitationHandler = vi.fn(async request => {
1595
+ expect(request.message).toBe('Please provide your information');
1596
+ expect(request.requestedSchema).toBeDefined();
1597
+ expect(request.requestedSchema.properties.name).toBeDefined();
1598
+
1599
+ return {
1600
+ action: 'accept' as const,
1601
+ content: {
1602
+ validated: true,
1603
+ },
1604
+ };
1605
+ });
1606
+
1607
+ elicitationClient = new InternalMastraMCPClient({
1608
+ name: 'elicitation-test-client',
1609
+ server: {
1610
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1611
+ },
1612
+ });
1613
+ elicitationClient.elicitation.onRequest(mockElicitationHandler);
1614
+ await elicitationClient.connect();
1615
+
1616
+ const tools = await elicitationClient.tools();
1617
+ const tool = tools['testElicitationTool'];
1618
+ expect(tool).toBeDefined();
1619
+
1620
+ const result = await tool.execute({
1621
+ context: {
1622
+ message: 'Please provide your information',
1623
+ },
1624
+ });
1625
+
1626
+ expect(mockElicitationHandler).toHaveBeenCalledTimes(1);
1627
+ expect(result.content[0].text).toContain('Elicitation response content does not match requested schema');
1628
+ });
1629
+
1630
+ it('should isolate elicitation handlers between different client connections', async () => {
1631
+ const client1Handler = vi.fn(async request => {
1632
+ expect(request.message).toBe('Please provide your information');
1633
+ expect(request.requestedSchema).toBeDefined();
1634
+ expect(request.requestedSchema.properties.name).toBeDefined();
1635
+
1636
+ return {
1637
+ action: 'accept' as const,
1638
+ content: {
1639
+ name: 'John Doe',
1640
+ email: 'john@example.com',
1641
+ },
1642
+ };
1643
+ });
1644
+ const client2Handler = vi.fn(async request => {
1645
+ expect(request.message).toBe('Please provide your information');
1646
+ expect(request.requestedSchema).toBeDefined();
1647
+ expect(request.requestedSchema.properties.name).toBeDefined();
1648
+
1649
+ return {
1650
+ action: 'accept' as const,
1651
+ content: {
1652
+ name: 'John Doe',
1653
+ email: 'john@example.com',
1654
+ },
1655
+ };
1656
+ });
1657
+
1658
+ // Create two independent client instances
1659
+ const elicitationClient1 = new MCPClient({
1660
+ id: 'elicitation-isolation-client-1',
1661
+ servers: {
1662
+ elicitation1: {
1663
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1664
+ },
1665
+ },
1666
+ });
1667
+
1668
+ const elicitationClient2 = new MCPClient({
1669
+ id: 'elicitation-isolation-client-2',
1670
+ servers: {
1671
+ elicitation2: {
1672
+ url: new URL(`http://localhost:${ELICITATION_PORT}/http`),
1673
+ },
1674
+ },
1675
+ });
1676
+
1677
+ // Each client registers its own independent handler
1678
+ elicitationClient1.elicitation.onRequest('elicitation1', client1Handler);
1679
+ elicitationClient2.elicitation.onRequest('elicitation2', client2Handler);
1680
+
1681
+ const tools = await elicitationClient1.getTools();
1682
+ const tool = tools['elicitation1_testElicitationTool'];
1683
+ expect(tool).toBeDefined();
1684
+ await tool.execute({
1685
+ context: {
1686
+ message: 'Please provide your information',
1687
+ },
1688
+ });
1689
+
1690
+ const tools2 = await elicitationClient2.getTools();
1691
+ const tool2 = tools2['elicitation2_testElicitationTool'];
1692
+ expect(tool2).toBeDefined();
1693
+
1694
+ // Verify handlers are isolated - they should not interfere with each other
1695
+ expect(client1Handler).toHaveBeenCalled();
1696
+ expect(client2Handler).not.toHaveBeenCalled();
1697
+ }, 10000);
1698
+ });
1699
+
1700
+ describe('MCPServer with Tool Output Schema', () => {
1701
+ let serverWithOutputSchema: MCPServer;
1702
+ let clientWithOutputSchema: MCPClient;
1703
+ const PORT = 9600 + Math.floor(Math.random() * 1000);
1704
+ let httpServerWithOutputSchema: http.Server;
1705
+
1706
+ const structuredTool: ToolsInput = {
1707
+ structuredTool: {
1708
+ description: 'A test tool with structured output',
1709
+ parameters: z.object({ input: z.string() }),
1710
+ outputSchema: z.object({
1711
+ processedInput: z.string(),
1712
+ timestamp: z.string(),
1713
+ }),
1714
+ execute: async ({ input }: { input: string }) => ({
1715
+ structuredContent: {
1716
+ processedInput: `processed: ${input}`,
1717
+ timestamp: mockDateISO,
1718
+ },
1719
+ }),
1720
+ },
1721
+ };
1722
+
1723
+ beforeAll(async () => {
1724
+ serverWithOutputSchema = new MCPServer({
1725
+ name: 'Test MCP Server with OutputSchema',
1726
+ version: '0.1.0',
1727
+ tools: structuredTool,
1728
+ });
1729
+
1730
+ httpServerWithOutputSchema = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
1731
+ const url = new URL(req.url || '', `http://localhost:${PORT}`);
1732
+ await serverWithOutputSchema.startHTTP({
1733
+ url,
1734
+ httpPath: '/http',
1735
+ req,
1736
+ res,
1737
+ });
1738
+ });
1739
+
1740
+ await new Promise<void>(resolve => httpServerWithOutputSchema.listen(PORT, () => resolve()));
1741
+
1742
+ clientWithOutputSchema = new MCPClient({
1743
+ servers: {
1744
+ local: {
1745
+ url: new URL(`http://localhost:${PORT}/http`),
1746
+ },
1747
+ },
1748
+ });
1749
+ });
1750
+
1751
+ afterAll(async () => {
1752
+ httpServerWithOutputSchema.closeAllConnections?.();
1753
+ await new Promise<void>(resolve =>
1754
+ httpServerWithOutputSchema.close(() => {
1755
+ resolve();
1756
+ }),
1757
+ );
1758
+ await serverWithOutputSchema.close();
1759
+ });
1760
+
1761
+ it('should list tool with outputSchema', async () => {
1762
+ const tools = await clientWithOutputSchema.getTools();
1763
+ const tool = tools['local_structuredTool'];
1764
+ expect(tool).toBeDefined();
1765
+ expect(tool.outputSchema).toBeDefined();
1766
+ });
1767
+
1768
+ it('should call tool and receive structuredContent', async () => {
1769
+ const tools = await clientWithOutputSchema.getTools();
1770
+ const tool = tools['local_structuredTool'];
1771
+ const result = await tool.execute({ context: { input: 'hello' } });
1772
+
1773
+ expect(result).toBeDefined();
1774
+ expect(result.structuredContent).toBeDefined();
1775
+ expect(result.structuredContent.processedInput).toBe('processed: hello');
1776
+ expect(result.structuredContent.timestamp).toBe(mockDateISO);
1777
+
1778
+ expect(result.content).toBeDefined();
1779
+ expect(result.content[0].type).toBe('text');
1780
+ expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent);
1781
+ });
1782
+ });