Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/mcp-server/src/utils/error-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ function jsonApiDetail(body: unknown, text?: string): string | null {
return null;
}

// Turn an agent RPC error into a human-readable message (AgentHttpError detail, else raw message).
// Turn an agent RPC error into a human-readable message: the JSON:API detail with its HTTP status
// when one can be extracted, else the raw message (which already carries the status).
export default function parseAgentError(error: unknown): string | null {
if (error instanceof AgentHttpError) {
return jsonApiDetail(error.body, error.responseText) ?? error.message;
const detail = jsonApiDetail(error.body, error.responseText);

return detail ? `${detail} (HTTP ${error.status})` : error.message;
}

if (error && typeof error === 'object' && 'message' in error) {
Expand Down
4 changes: 3 additions & 1 deletion packages/mcp-server/src/utils/tool-with-logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {

import { z } from 'zod';

import parseAgentError from './error-parser';

// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -114,7 +116,7 @@ export default function registerToolWithLogging<
let message: string;

try {
message = error instanceof Error ? error.message : JSON.stringify(error) ?? String(error);
message = parseAgentError(error) ?? JSON.stringify(error) ?? String(error);
Comment thread
hercemer42 marked this conversation as resolved.
Comment thread
hercemer42 marked this conversation as resolved.
} catch {
message = String(error);
}
Expand Down
14 changes: 10 additions & 4 deletions packages/mcp-server/test/utils/error-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import { AgentHttpError } from '@forestadmin/agent-client';
import parseAgentError from '../../src/utils/error-parser';

describe('parseAgentError', () => {
it('extracts the JSON:API detail from the parsed body', () => {
it('appends the HTTP status to the JSON:API detail from the parsed body', () => {
const error = new AgentHttpError(400, {
errors: [{ name: 'ValidationError', detail: 'Invalid filters provided' }],
});

expect(parseAgentError(error)).toBe('Invalid filters provided');
expect(parseAgentError(error)).toBe('Invalid filters provided (HTTP 400)');
});

it('falls back to parsing responseText when the body is not a parsed object', () => {
it('appends the HTTP status to the JSON:API detail parsed from responseText', () => {
const error = new AgentHttpError(
400,
undefined,
JSON.stringify({ errors: [{ detail: 'From raw text' }] }),
);

expect(parseAgentError(error)).toBe('From raw text');
expect(parseAgentError(error)).toBe('From raw text (HTTP 400)');
});

it('returns the generic HTTP message when the body has no JSON:API detail', () => {
Expand All @@ -33,6 +33,12 @@ describe('parseAgentError', () => {
);
});

it('does not append a second HTTP status when the body is not JSON:API', () => {
expect(parseAgentError(new AgentHttpError(500, '<html>Internal Server Error</html>'))).toBe(
'Agent responded with HTTP 500',
);
});

it('returns the message of a plain Error', () => {
expect(parseAgentError(new Error('Plain error message'))).toBe('Plain error message');
});
Expand Down
41 changes: 41 additions & 0 deletions packages/mcp-server/test/utils/tool-with-logging.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AgentHttpError } from '@forestadmin/agent-client';
import { z } from 'zod';

import registerToolWithLogging from '../../src/utils/tool-with-logging';
Expand Down Expand Up @@ -173,6 +174,46 @@ describe('registerToolWithLogging', () => {
});
});

it('should surface the agent error detail and HTTP status for an AgentHttpError', async () => {
const error = new AgentHttpError(422, {
errors: [{ name: 'ValidationError', detail: 'The value "reason" is required' }],
});
const handler = jest.fn().mockRejectedValue(error);

registerToolWithLogging(mockMcpServer as never, 'test-tool', toolConfig, handler, mockLogger);

const result = await registeredHandler({ name: 'test', count: 42 }, {});
expect(result).toEqual({
content: [{ type: 'text', text: 'The value "reason" is required (HTTP 422)' }],
isError: true,
});
});

it('should return the generic HTTP message without a doubled status when the body is not JSON:API', async () => {
const error = new AgentHttpError(500, '<html>Internal Server Error</html>');
const handler = jest.fn().mockRejectedValue(error);

registerToolWithLogging(mockMcpServer as never, 'test-tool', toolConfig, handler, mockLogger);

const result = await registeredHandler({ name: 'test', count: 42 }, {});
expect(result).toEqual({
content: [{ type: 'text', text: 'Agent responded with HTTP 500' }],
isError: true,
});
});

it('should use the message of a thrown non-Error object that has one', async () => {
const handler = jest.fn().mockRejectedValue({ message: 'boom', code: 'X' });

registerToolWithLogging(mockMcpServer as never, 'test-tool', toolConfig, handler, mockLogger);

const result = await registeredHandler({ name: 'test', count: 42 }, {});
expect(result).toEqual({
content: [{ type: 'text', text: 'boom' }],
isError: true,
});
});

it('should call handler even when validation fails', async () => {
const handler = jest.fn().mockResolvedValue({ content: [{ type: 'text', text: 'ok' }] });

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/test/utils/with-activity-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ describe('withActivityLog', () => {
}),
).rejects.toThrow('Context: Invalid field value');

expect(errorEnhancer).toHaveBeenCalledWith('Invalid field value', agentError);
expect(errorEnhancer).toHaveBeenCalledWith('Invalid field value (HTTP 400)', agentError);
});
});
});
Loading