|
1 | | -import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' |
| 1 | +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest' |
2 | 2 | import { isHosted } from '@/lib/environment' |
3 | 3 | import { getAllBlocks } from '@/blocks' |
4 | 4 | import { BlockType } from '@/executor/consts' |
@@ -1380,5 +1380,341 @@ describe('AgentBlockHandler', () => { |
1380 | 1380 | expect(requestBody.provider).toBe('openai') |
1381 | 1381 | expect(requestBody.model).toBe('gpt-5') |
1382 | 1382 | }) |
| 1383 | + |
| 1384 | + it('should handle MCP tools in agent execution', async () => { |
| 1385 | + mockExecuteTool.mockImplementation((toolId, params, skipProxy, skipPostProcess, context) => { |
| 1386 | + if (toolId.startsWith('mcp-')) { |
| 1387 | + return Promise.resolve({ |
| 1388 | + success: true, |
| 1389 | + output: { |
| 1390 | + content: [ |
| 1391 | + { |
| 1392 | + type: 'text', |
| 1393 | + text: `MCP tool ${toolId} executed with params: ${JSON.stringify(params)}`, |
| 1394 | + }, |
| 1395 | + ], |
| 1396 | + }, |
| 1397 | + }) |
| 1398 | + } |
| 1399 | + return Promise.resolve({ success: false, error: 'Unknown tool' }) |
| 1400 | + }) |
| 1401 | + |
| 1402 | + mockFetch.mockImplementationOnce(() => { |
| 1403 | + return Promise.resolve({ |
| 1404 | + ok: true, |
| 1405 | + headers: { |
| 1406 | + get: (name: string) => { |
| 1407 | + if (name === 'Content-Type') return 'application/json' |
| 1408 | + if (name === 'X-Execution-Data') return null |
| 1409 | + return null |
| 1410 | + }, |
| 1411 | + }, |
| 1412 | + json: () => |
| 1413 | + Promise.resolve({ |
| 1414 | + content: 'I will use MCP tools to help you.', |
| 1415 | + model: 'gpt-4o', |
| 1416 | + tokens: { prompt: 15, completion: 25, total: 40 }, |
| 1417 | + toolCalls: [ |
| 1418 | + { |
| 1419 | + name: 'mcp-server1-list_files', |
| 1420 | + arguments: { path: '/tmp' }, |
| 1421 | + result: { |
| 1422 | + success: true, |
| 1423 | + output: { content: [{ type: 'text', text: 'Files listed' }] }, |
| 1424 | + }, |
| 1425 | + }, |
| 1426 | + { |
| 1427 | + name: 'mcp-server2-search', |
| 1428 | + arguments: { query: 'test', limit: 5 }, |
| 1429 | + result: { |
| 1430 | + success: true, |
| 1431 | + output: { content: [{ type: 'text', text: 'Search results' }] }, |
| 1432 | + }, |
| 1433 | + }, |
| 1434 | + ], |
| 1435 | + timing: { total: 150 }, |
| 1436 | + }), |
| 1437 | + }) |
| 1438 | + }) |
| 1439 | + |
| 1440 | + const inputs = { |
| 1441 | + model: 'gpt-4o', |
| 1442 | + userPrompt: 'List files and search for test data', |
| 1443 | + apiKey: 'test-api-key', |
| 1444 | + tools: [ |
| 1445 | + { |
| 1446 | + type: 'mcp', |
| 1447 | + title: 'List Files', |
| 1448 | + schema: { |
| 1449 | + function: { |
| 1450 | + name: 'mcp-server1-list_files', |
| 1451 | + description: 'List files in directory', |
| 1452 | + parameters: { |
| 1453 | + type: 'object', |
| 1454 | + properties: { |
| 1455 | + path: { type: 'string', description: 'Directory path' }, |
| 1456 | + }, |
| 1457 | + }, |
| 1458 | + }, |
| 1459 | + }, |
| 1460 | + usageControl: 'auto' as const, |
| 1461 | + }, |
| 1462 | + { |
| 1463 | + type: 'mcp', |
| 1464 | + title: 'Search', |
| 1465 | + schema: { |
| 1466 | + function: { |
| 1467 | + name: 'mcp-server2-search', |
| 1468 | + description: 'Search for data', |
| 1469 | + parameters: { |
| 1470 | + type: 'object', |
| 1471 | + properties: { |
| 1472 | + query: { type: 'string', description: 'Search query' }, |
| 1473 | + limit: { type: 'number', description: 'Result limit' }, |
| 1474 | + }, |
| 1475 | + }, |
| 1476 | + }, |
| 1477 | + }, |
| 1478 | + usageControl: 'auto' as const, |
| 1479 | + }, |
| 1480 | + ], |
| 1481 | + } |
| 1482 | + |
| 1483 | + const mcpContext = { |
| 1484 | + ...mockContext, |
| 1485 | + workspaceId: 'test-workspace-123', |
| 1486 | + } |
| 1487 | + |
| 1488 | + mockGetProviderFromModel.mockReturnValue('openai') |
| 1489 | + |
| 1490 | + const result = await handler.execute(mockBlock, inputs, mcpContext) |
| 1491 | + |
| 1492 | + expect((result as any).content).toBe('I will use MCP tools to help you.') |
| 1493 | + expect((result as any).toolCalls.count).toBe(2) |
| 1494 | + expect((result as any).toolCalls.list).toHaveLength(2) |
| 1495 | + |
| 1496 | + expect((result as any).toolCalls.list[0].name).toBe('mcp-server1-list_files') |
| 1497 | + expect((result as any).toolCalls.list[0].result.success).toBe(true) |
| 1498 | + expect((result as any).toolCalls.list[1].name).toBe('mcp-server2-search') |
| 1499 | + expect((result as any).toolCalls.list[1].result.success).toBe(true) |
| 1500 | + }) |
| 1501 | + |
| 1502 | + it('should handle MCP tool execution errors', async () => { |
| 1503 | + mockExecuteTool.mockImplementation((toolId, params) => { |
| 1504 | + if (toolId === 'mcp-server1-failing_tool') { |
| 1505 | + return Promise.resolve({ |
| 1506 | + success: false, |
| 1507 | + error: 'MCP server connection failed', |
| 1508 | + }) |
| 1509 | + } |
| 1510 | + return Promise.resolve({ success: false, error: 'Unknown tool' }) |
| 1511 | + }) |
| 1512 | + |
| 1513 | + mockFetch.mockImplementationOnce(() => { |
| 1514 | + return Promise.resolve({ |
| 1515 | + ok: true, |
| 1516 | + headers: { |
| 1517 | + get: (name: string) => { |
| 1518 | + if (name === 'Content-Type') return 'application/json' |
| 1519 | + if (name === 'X-Execution-Data') return null |
| 1520 | + return null |
| 1521 | + }, |
| 1522 | + }, |
| 1523 | + json: () => |
| 1524 | + Promise.resolve({ |
| 1525 | + content: 'Let me try to use this tool.', |
| 1526 | + model: 'gpt-4o', |
| 1527 | + tokens: { prompt: 10, completion: 15, total: 25 }, |
| 1528 | + toolCalls: [ |
| 1529 | + { |
| 1530 | + name: 'mcp-server1-failing_tool', |
| 1531 | + arguments: { param: 'value' }, |
| 1532 | + result: { |
| 1533 | + success: false, |
| 1534 | + error: 'MCP server connection failed', |
| 1535 | + }, |
| 1536 | + }, |
| 1537 | + ], |
| 1538 | + timing: { total: 100 }, |
| 1539 | + }), |
| 1540 | + }) |
| 1541 | + }) |
| 1542 | + |
| 1543 | + const inputs = { |
| 1544 | + model: 'gpt-4o', |
| 1545 | + userPrompt: 'Try to use the failing tool', |
| 1546 | + apiKey: 'test-api-key', |
| 1547 | + tools: [ |
| 1548 | + { |
| 1549 | + type: 'mcp', |
| 1550 | + title: 'Failing Tool', |
| 1551 | + schema: { |
| 1552 | + function: { |
| 1553 | + name: 'mcp-server1-failing_tool', |
| 1554 | + description: 'A tool that will fail', |
| 1555 | + parameters: { |
| 1556 | + type: 'object', |
| 1557 | + properties: { |
| 1558 | + param: { type: 'string' }, |
| 1559 | + }, |
| 1560 | + }, |
| 1561 | + }, |
| 1562 | + }, |
| 1563 | + usageControl: 'auto' as const, |
| 1564 | + }, |
| 1565 | + ], |
| 1566 | + } |
| 1567 | + |
| 1568 | + const mcpContext = { |
| 1569 | + ...mockContext, |
| 1570 | + workspaceId: 'test-workspace-123', |
| 1571 | + } |
| 1572 | + |
| 1573 | + mockGetProviderFromModel.mockReturnValue('openai') |
| 1574 | + |
| 1575 | + const result = await handler.execute(mockBlock, inputs, mcpContext) |
| 1576 | + |
| 1577 | + expect((result as any).content).toBe('Let me try to use this tool.') |
| 1578 | + expect((result as any).toolCalls.count).toBe(1) |
| 1579 | + expect((result as any).toolCalls.list[0].result.success).toBe(false) |
| 1580 | + expect((result as any).toolCalls.list[0].result.error).toBe('MCP server connection failed') |
| 1581 | + }) |
| 1582 | + |
| 1583 | + it('should transform MCP tools correctly for agent execution', async () => { |
| 1584 | + const inputs = { |
| 1585 | + model: 'gpt-4o', |
| 1586 | + userPrompt: 'Use MCP tools to help me', |
| 1587 | + apiKey: 'test-api-key', |
| 1588 | + tools: [ |
| 1589 | + { |
| 1590 | + type: 'mcp', |
| 1591 | + title: 'Read File', |
| 1592 | + schema: { |
| 1593 | + function: { |
| 1594 | + name: 'mcp-filesystem-read_file', |
| 1595 | + description: 'Read file from filesystem', |
| 1596 | + parameters: { type: 'object', properties: {} }, |
| 1597 | + }, |
| 1598 | + }, |
| 1599 | + usageControl: 'auto' as const, |
| 1600 | + }, |
| 1601 | + { |
| 1602 | + type: 'mcp', |
| 1603 | + title: 'Web Search', |
| 1604 | + schema: { |
| 1605 | + function: { |
| 1606 | + name: 'mcp-web-search', |
| 1607 | + description: 'Search the web', |
| 1608 | + parameters: { type: 'object', properties: {} }, |
| 1609 | + }, |
| 1610 | + }, |
| 1611 | + usageControl: 'force' as const, |
| 1612 | + }, |
| 1613 | + ], |
| 1614 | + } |
| 1615 | + |
| 1616 | + mockGetProviderFromModel.mockReturnValue('openai') |
| 1617 | + |
| 1618 | + mockFetch.mockImplementationOnce(() => { |
| 1619 | + return Promise.resolve({ |
| 1620 | + ok: true, |
| 1621 | + headers: { |
| 1622 | + get: (name: string) => { |
| 1623 | + if (name === 'Content-Type') return 'application/json' |
| 1624 | + if (name === 'X-Execution-Data') return null |
| 1625 | + return null |
| 1626 | + }, |
| 1627 | + }, |
| 1628 | + json: () => |
| 1629 | + Promise.resolve({ |
| 1630 | + content: 'Used MCP tools successfully', |
| 1631 | + model: 'gpt-4o', |
| 1632 | + tokens: { prompt: 20, completion: 30, total: 50 }, |
| 1633 | + toolCalls: [], |
| 1634 | + timing: { total: 200 }, |
| 1635 | + }), |
| 1636 | + }) |
| 1637 | + }) |
| 1638 | + |
| 1639 | + mockTransformBlockTool.mockImplementation((tool: any) => ({ |
| 1640 | + id: tool.schema?.function?.name || `mcp-${tool.title.toLowerCase().replace(' ', '-')}`, |
| 1641 | + name: tool.schema?.function?.name || tool.title, |
| 1642 | + description: tool.schema?.function?.description || `MCP tool: ${tool.title}`, |
| 1643 | + parameters: tool.schema?.function?.parameters || { type: 'object', properties: {} }, |
| 1644 | + usageControl: tool.usageControl, |
| 1645 | + })) |
| 1646 | + |
| 1647 | + const result = await handler.execute(mockBlock, inputs, mockContext) |
| 1648 | + |
| 1649 | + // Verify that the agent executed successfully with MCP tools |
| 1650 | + expect(result).toBeDefined() |
| 1651 | + expect(mockFetch).toHaveBeenCalled() |
| 1652 | + |
| 1653 | + // Verify the agent returns the expected response format |
| 1654 | + expect((result as any).content).toBe('Used MCP tools successfully') |
| 1655 | + expect((result as any).model).toBe('gpt-4o') |
| 1656 | + }) |
| 1657 | + |
| 1658 | + it('should provide workspaceId context for MCP tool execution', async () => { |
| 1659 | + let capturedContext: any |
| 1660 | + mockExecuteTool.mockImplementation((toolId, params, skipProxy, skipPostProcess, context) => { |
| 1661 | + capturedContext = context |
| 1662 | + if (toolId.startsWith('mcp-')) { |
| 1663 | + return Promise.resolve({ |
| 1664 | + success: true, |
| 1665 | + output: { content: [{ type: 'text', text: 'Success' }] }, |
| 1666 | + }) |
| 1667 | + } |
| 1668 | + return Promise.resolve({ success: false, error: 'Unknown tool' }) |
| 1669 | + }) |
| 1670 | + |
| 1671 | + mockFetch.mockImplementationOnce(() => { |
| 1672 | + return Promise.resolve({ |
| 1673 | + ok: true, |
| 1674 | + headers: { |
| 1675 | + get: (name: string) => (name === 'Content-Type' ? 'application/json' : null), |
| 1676 | + }, |
| 1677 | + json: () => |
| 1678 | + Promise.resolve({ |
| 1679 | + content: 'Using MCP tool', |
| 1680 | + model: 'gpt-4o', |
| 1681 | + tokens: { prompt: 10, completion: 10, total: 20 }, |
| 1682 | + toolCalls: [{ name: 'mcp-test-tool', arguments: {} }], |
| 1683 | + timing: { total: 50 }, |
| 1684 | + }), |
| 1685 | + }) |
| 1686 | + }) |
| 1687 | + |
| 1688 | + const inputs = { |
| 1689 | + model: 'gpt-4o', |
| 1690 | + userPrompt: 'Test MCP context', |
| 1691 | + apiKey: 'test-api-key', |
| 1692 | + tools: [ |
| 1693 | + { |
| 1694 | + type: 'mcp', |
| 1695 | + title: 'Test Tool', |
| 1696 | + schema: { |
| 1697 | + function: { |
| 1698 | + name: 'mcp-test-tool', |
| 1699 | + description: 'Test MCP tool', |
| 1700 | + parameters: { type: 'object', properties: {} }, |
| 1701 | + }, |
| 1702 | + }, |
| 1703 | + usageControl: 'auto' as const, |
| 1704 | + }, |
| 1705 | + ], |
| 1706 | + } |
| 1707 | + |
| 1708 | + const contextWithWorkspace = { |
| 1709 | + ...mockContext, |
| 1710 | + workspaceId: 'test-workspace-456', |
| 1711 | + } |
| 1712 | + |
| 1713 | + mockGetProviderFromModel.mockReturnValue('openai') |
| 1714 | + |
| 1715 | + await handler.execute(mockBlock, inputs, contextWithWorkspace) |
| 1716 | + |
| 1717 | + expect(contextWithWorkspace.workspaceId).toBe('test-workspace-456') |
| 1718 | + }) |
1383 | 1719 | }) |
1384 | 1720 | }) |
0 commit comments