diff --git a/src/main/mcpServers/hub/__tests__/hub.test.ts b/src/main/mcpServers/hub/__tests__/hub.test.ts index 92a5b65578..51ec727ee9 100644 --- a/src/main/mcpServers/hub/__tests__/hub.test.ts +++ b/src/main/mcpServers/hub/__tests__/hub.test.ts @@ -176,6 +176,7 @@ describe('HubServer Integration', () => { const execOutput = JSON.parse(execResult.content[0].text) expect(execOutput.error).toBe('test error') + expect(execOutput.isError).toBe(true) }) }) @@ -213,6 +214,7 @@ describe('HubServer Integration', () => { expect(execOutput.error).toBe('Execution timed out after 60000ms') expect(execOutput.result).toBeUndefined() + expect(execOutput.isError).toBe(true) expect(execOutput.logs).toContain('[log] starting') expect(vi.mocked(mcpService.abortTool)).toHaveBeenCalled() }) diff --git a/src/main/mcpServers/hub/__tests__/runtime.test.ts b/src/main/mcpServers/hub/__tests__/runtime.test.ts index 86664a2021..5268aa6dec 100644 --- a/src/main/mcpServers/hub/__tests__/runtime.test.ts +++ b/src/main/mcpServers/hub/__tests__/runtime.test.ts @@ -85,6 +85,7 @@ describe('Runtime', () => { expect(result.result).toBeUndefined() expect(result.error).toBe('test error') + expect(result.isError).toBe(true) }) it('supports parallel helper', async () => { @@ -139,5 +140,20 @@ describe('Runtime', () => { expect(result.result).toBe(30) }) + + it('stops execution when a tool throws', async () => { + const runtime = new Runtime() + const tools = [ + createMockTool({ + functionName: 'server__failing_tool' + }) + ] + + const result = await runtime.execute('return await server__failing_tool({})', tools) + + expect(result.result).toBeUndefined() + expect(result.error).toBe('Tool failed') + expect(result.isError).toBe(true) + }) }) }) diff --git a/src/main/mcpServers/hub/index.ts b/src/main/mcpServers/hub/index.ts index c3abbd517c..6647a3c803 100644 --- a/src/main/mcpServers/hub/index.ts +++ b/src/main/mcpServers/hub/index.ts @@ -175,7 +175,8 @@ export class HubServer { type: 'text', text: JSON.stringify(result, null, 2) } - ] + ], + isError: result.isError } } } diff --git a/src/main/mcpServers/hub/mcp-bridge.ts b/src/main/mcpServers/hub/mcp-bridge.ts index b759d83b35..9e45eeba0d 100644 --- a/src/main/mcpServers/hub/mcp-bridge.ts +++ b/src/main/mcpServers/hub/mcp-bridge.ts @@ -4,7 +4,7 @@ */ import mcpService from '@main/services/MCPService' import { generateMcpToolFunctionName } from '@shared/mcp' -import type { MCPTool } from '@types' +import type { MCPCallToolResponse, MCPTool, MCPToolResultContent } from '@types' import type { GeneratedTool } from './types' @@ -47,10 +47,12 @@ export const callMcpTool = async (functionName: string, params: unknown, callId? } const toolId = `${retryToolInfo.serverId}__${retryToolInfo.toolName}` const result = await mcpService.callToolById(toolId, params, callId) + throwIfToolError(result) return extractToolResult(result) } const toolId = `${toolInfo.serverId}__${toolInfo.toolName}` const result = await mcpService.callToolById(toolId, params, callId) + throwIfToolError(result) return extractToolResult(result) } @@ -58,7 +60,7 @@ export const abortMcpTool = async (callId: string): Promise => { return mcpService.abortTool(null as unknown as Electron.IpcMainInvokeEvent, callId) } -function extractToolResult(result: { content: Array<{ type: string; text?: string }> }): unknown { +function extractToolResult(result: MCPCallToolResponse): unknown { if (!result.content || result.content.length === 0) { return null } @@ -74,3 +76,21 @@ function extractToolResult(result: { content: Array<{ type: string; text?: strin return result.content } + +function throwIfToolError(result: MCPCallToolResponse): void { + if (!result.isError) { + return + } + + const textContent = extractTextContent(result.content) + throw new Error(textContent ?? 'Tool execution failed') +} + +function extractTextContent(content: MCPToolResultContent[] | undefined): string | undefined { + if (!content || content.length === 0) { + return undefined + } + + const textBlock = content.find((item) => item.type === 'text' && item.text) + return textBlock?.text +} diff --git a/src/main/mcpServers/hub/runtime.ts b/src/main/mcpServers/hub/runtime.ts index c266157a09..dd33fc9ff0 100644 --- a/src/main/mcpServers/hub/runtime.ts +++ b/src/main/mcpServers/hub/runtime.ts @@ -103,7 +103,8 @@ export class Runtime { { result: undefined, logs: resolvedLogs.length > 0 ? resolvedLogs : undefined, - error: errorMessage + error: errorMessage, + isError: true }, terminateWorker ) diff --git a/src/main/mcpServers/hub/types.ts b/src/main/mcpServers/hub/types.ts index 2bff26b8dc..1c24ac7e77 100644 --- a/src/main/mcpServers/hub/types.ts +++ b/src/main/mcpServers/hub/types.ts @@ -26,10 +26,11 @@ export interface ExecInput { code: string } -export interface ExecOutput { +export type ExecOutput = { result: unknown logs?: string[] error?: string + isError?: boolean } export interface ToolRegistryOptions { diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index f106dd9faf..013bdb6ad7 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -195,11 +195,7 @@ class McpService { * Call a tool by its full ID (serverId__toolName format). * Used by Hub server's runtime. */ - public async callToolById( - toolId: string, - params: unknown, - callId?: string - ): Promise<{ content: Array<{ type: string; text?: string }> }> { + public async callToolById(toolId: string, params: unknown, callId?: string): Promise { const parts = toolId.split('__') if (parts.length < 2) { throw new Error(`Invalid tool ID format: ${toolId}`)