/** * Unit tests for the MCP module, src/mcp.ts */ import { jest } from '@jest/globals' import * as core from '../__fixtures__/core.js' // Mock MCP SDK // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockConnect = jest.fn() as jest.MockedFunction // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockListTools = jest.fn() as jest.MockedFunction // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockCallTool = jest.fn() as jest.MockedFunction const mockClient = { connect: mockConnect, listTools: mockListTools, callTool: mockCallTool // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any jest.unstable_mockModule('@modelcontextprotocol/sdk/client/index.js', () => ({ Client: jest.fn(() => mockClient) })) jest.unstable_mockModule( '@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({ StreamableHTTPClientTransport: jest.fn() }) ) jest.unstable_mockModule('@actions/core', () => core) // Import the module being tested const { connectToGitHubMCP, executeToolCall, executeToolCalls } = await import( '../src/mcp.js' ) describe('mcp.ts', () => { beforeEach(() => { jest.clearAllMocks() }) describe('connectToGitHubMCP', () => { it('successfully connects to MCP server and retrieves tools', async () => { const token = 'test-token' const mockTools = [ { name: 'test-tool-1', description: 'Test tool 1', inputSchema: { type: 'object', properties: {} } }, { name: 'test-tool-2', description: 'Test tool 2', inputSchema: { type: 'object', properties: { param: { type: 'string' } } } } ] mockConnect.mockResolvedValue(undefined) mockListTools.mockResolvedValue({ tools: mockTools }) const result = await connectToGitHubMCP(token) expect(result).not.toBeNull() expect(result?.client).toBe(mockClient) expect(result?.tools).toHaveLength(2) expect(result?.tools[0]).toEqual({ type: 'function', function: { name: 'test-tool-1', description: 'Test tool 1', parameters: { type: 'object', properties: {} } } }) expect(core.info).toHaveBeenCalledWith( 'Connecting to GitHub MCP server...' ) expect(core.info).toHaveBeenCalledWith( 'Successfully connected to GitHub MCP server' ) expect(core.info).toHaveBeenCalledWith( 'Retrieved 2 tools from GitHub MCP server' ) expect(core.info).toHaveBeenCalledWith( 'Mapped 2 GitHub MCP tools for Azure AI Inference' ) }) it('returns null when connection fails', async () => { const token = 'test-token' const connectionError = new Error('Connection failed') mockConnect.mockRejectedValue(connectionError) const result = await connectToGitHubMCP(token) expect(result).toBeNull() expect(core.warning).toHaveBeenCalledWith( 'Failed to connect to GitHub MCP server: Error: Connection failed' ) }) it('handles empty tools list', async () => { const token = 'test-token' mockConnect.mockResolvedValue(undefined) mockListTools.mockResolvedValue({ tools: [] }) const result = await connectToGitHubMCP(token) expect(result).not.toBeNull() expect(result?.tools).toHaveLength(0) expect(core.info).toHaveBeenCalledWith( 'Retrieved 0 tools from GitHub MCP server' ) expect(core.info).toHaveBeenCalledWith( 'Mapped 0 GitHub MCP tools for Azure AI Inference' ) }) it('handles undefined tools list', async () => { const token = 'test-token' mockConnect.mockResolvedValue(undefined) mockListTools.mockResolvedValue({}) const result = await connectToGitHubMCP(token) expect(result).not.toBeNull() expect(result?.tools).toHaveLength(0) expect(core.info).toHaveBeenCalledWith( 'Retrieved 0 tools from GitHub MCP server' ) }) }) describe('executeToolCall', () => { it('successfully executes a tool call', async () => { const toolCall = { id: 'call-123', type: 'function', function: { name: 'test-tool', arguments: '{"param": "value"}' } } const toolResult = { content: [{ type: 'text', text: 'Tool execution result' }] } mockCallTool.mockResolvedValue(toolResult) const result = await executeToolCall(mockClient, toolCall) expect(mockCallTool).toHaveBeenCalledWith({ name: 'test-tool', arguments: { param: 'value' } }) expect(result).toEqual({ tool_call_id: 'call-123', role: 'tool', name: 'test-tool', content: JSON.stringify(toolResult.content) }) expect(core.info).toHaveBeenCalledWith( 'Executing GitHub MCP tool: test-tool with args: {"param": "value"}' ) expect(core.info).toHaveBeenCalledWith( 'GitHub MCP tool test-tool executed successfully' ) }) it('handles tool execution errors gracefully', async () => { const toolCall = { id: 'call-456', type: 'function', function: { name: 'failing-tool', arguments: '{"param": "value"}' } } const toolError = new Error('Tool execution failed') mockCallTool.mockRejectedValue(toolError) const result = await executeToolCall(mockClient, toolCall) expect(result).toEqual({ tool_call_id: 'call-456', role: 'tool', name: 'failing-tool', content: 'Error: Error: Tool execution failed' }) expect(core.warning).toHaveBeenCalledWith( 'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed' ) }) it('handles invalid JSON arguments', async () => { const toolCall = { id: 'call-789', type: 'function', function: { name: 'test-tool', arguments: 'invalid-json' } } const result = await executeToolCall(mockClient, toolCall) expect(result.tool_call_id).toBe('call-789') expect(result.role).toBe('tool') expect(result.name).toBe('test-tool') expect(result.content).toContain('Error:') expect(core.warning).toHaveBeenCalledWith( expect.stringContaining('Failed to execute GitHub MCP tool test-tool:') ) }) }) describe('executeToolCalls', () => { it('executes multiple tool calls successfully', async () => { const toolCalls = [ { id: 'call-1', type: 'function', function: { name: 'tool-1', arguments: '{}' } }, { id: 'call-2', type: 'function', function: { name: 'tool-2', arguments: '{"param": "value"}' } } ] mockCallTool .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Result 1' }] }) .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Result 2' }] }) const results = await executeToolCalls(mockClient, toolCalls) expect(results).toHaveLength(2) expect(results[0].tool_call_id).toBe('call-1') expect(results[1].tool_call_id).toBe('call-2') expect(mockCallTool).toHaveBeenCalledTimes(2) }) it('handles empty tool calls array', async () => { const results = await executeToolCalls(mockClient, []) expect(results).toHaveLength(0) expect(mockCallTool).not.toHaveBeenCalled() }) it('continues execution even if some tools fail', async () => { const toolCalls = [ { id: 'call-1', type: 'function', function: { name: 'tool-1', arguments: '{}' } }, { id: 'call-2', type: 'function', function: { name: 'tool-2', arguments: '{}' } } ] mockCallTool .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Result 1' }] }) .mockRejectedValueOnce(new Error('Tool 2 failed')) const results = await executeToolCalls(mockClient, toolCalls) expect(results).toHaveLength(2) expect(results[0].content).toContain('Result 1') expect(results[1].content).toContain('Error:') }) }) })