bug(mcp): catch errors reported by GitHub MCP (#6194)
This commit is contained in:
parent
a01db2cfd5
commit
f47af1607a
|
@ -185,6 +185,98 @@ describe('DiscoveredMCPTool', () => {
|
||||||
).rejects.toThrow(expectedError);
|
).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 () => {
|
it('should handle a simple text response correctly', async () => {
|
||||||
const params = { query: 'test' };
|
const params = { query: 'test' };
|
||||||
const successMessage = 'This is a success message.';
|
const successMessage = 'This is a success message.';
|
||||||
|
|
|
@ -104,6 +104,28 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
|
||||||
return confirmationDetails;
|
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> {
|
async execute(): Promise<ToolResult> {
|
||||||
const functionCalls: FunctionCall[] = [
|
const functionCalls: FunctionCall[] = [
|
||||||
{
|
{
|
||||||
|
@ -113,6 +135,14 @@ class DiscoveredMCPToolInvocation extends BaseToolInvocation<
|
||||||
];
|
];
|
||||||
|
|
||||||
const rawResponseParts = await this.mcpTool.callTool(functionCalls);
|
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);
|
const transformedParts = transformMcpContentToParts(rawResponseParts);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue