bug(mcp): catch errors reported by GitHub MCP (#6194)

This commit is contained in:
Lee James 2025-08-14 18:30:05 -04:00 committed by GitHub
parent a01db2cfd5
commit f47af1607a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 122 additions and 0 deletions

View File

@ -185,6 +185,98 @@ describe('DiscoveredMCPTool', () => {
).rejects.toThrow(expectedError);
});
it.each([
{ isErrorValue: true, description: 'true (bool)' },
{ isErrorValue: 'true', description: '"true" (str)' },
])(
'should consider a ToolResult with isError $description to be a failure',
async ({ isErrorValue }) => {
const tool = new DiscoveredMCPTool(
mockCallableToolInstance,
serverName,
serverToolName,
baseDescription,
inputSchema,
);
const params = { param: 'isErrorTrueCase' };
const errorResponse = { isError: isErrorValue };
const mockMcpToolResponseParts: Part[] = [
{
functionResponse: {
name: serverToolName,
response: { error: errorResponse },
},
},
];
mockCallTool.mockResolvedValue(mockMcpToolResponseParts);
const expectedError = new Error(
`MCP tool '${serverToolName}' reported tool error with response: ${JSON.stringify(
mockMcpToolResponseParts,
)}`,
);
const invocation = tool.build(params);
await expect(
invocation.execute(new AbortController().signal),
).rejects.toThrow(expectedError);
},
);
it.each([
{ isErrorValue: false, description: 'false (bool)' },
{ isErrorValue: 'false', description: '"false" (str)' },
])(
'should consider a ToolResult with isError ${description} to be a success',
async ({ isErrorValue }) => {
const tool = new DiscoveredMCPTool(
mockCallableToolInstance,
serverName,
serverToolName,
baseDescription,
inputSchema,
);
const params = { param: 'isErrorFalseCase' };
const mockToolSuccessResultObject = {
success: true,
details: 'executed',
};
const mockFunctionResponseContent = [
{
type: 'text',
text: JSON.stringify(mockToolSuccessResultObject),
},
];
const errorResponse = { isError: isErrorValue };
const mockMcpToolResponseParts: Part[] = [
{
functionResponse: {
name: serverToolName,
response: {
error: errorResponse,
content: mockFunctionResponseContent,
},
},
},
];
mockCallTool.mockResolvedValue(mockMcpToolResponseParts);
const invocation = tool.build(params);
const toolResult = await invocation.execute(
new AbortController().signal,
);
const stringifiedResponseContent = JSON.stringify(
mockToolSuccessResultObject,
);
expect(toolResult.llmContent).toEqual([
{ text: stringifiedResponseContent },
]);
expect(toolResult.returnDisplay).toBe(stringifiedResponseContent);
},
);
it('should handle a simple text response correctly', async () => {
const params = { query: 'test' };
const successMessage = 'This is a success message.';

View File

@ -104,6 +104,28 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
return confirmationDetails;
}
// Determine if the response contains tool errors
// This is needed because CallToolResults should return errors inside the response.
// ref: https://modelcontextprotocol.io/specification/2025-06-18/schema#calltoolresult
isMCPToolError(rawResponseParts: Part[]): boolean {
const functionResponse = rawResponseParts?.[0]?.functionResponse;
const response = functionResponse?.response;
interface McpError {
isError?: boolean | string;
}
if (response) {
const error = (response as { error?: McpError })?.error;
const isError = error?.isError;
if (error && (isError === true || isError === 'true')) {
return true;
}
}
return false;
}
async execute(): Promise<ToolResult> {
const functionCalls: FunctionCall[] = [
{
@ -113,6 +135,14 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
];
const rawResponseParts = await this.mcpTool.callTool(functionCalls);
// Ensure the response is not an error
if (this.isMCPToolError(rawResponseParts)) {
throw new Error(
`MCP tool '${this.serverToolName}' reported tool error with response: ${JSON.stringify(rawResponseParts)}`,
);
}
const transformedParts = transformMcpContentToParts(rawResponseParts);
return {