scrum4me-mcp/src/errors.ts
Madhura68 ea1c94b05b fix: wrap non-object values in toolJson, add e2e smoke test
The MCP SDK rejects tools/call results where structuredContent is not a
record — array returns from list_products triggered an MCP error code
-32602. toolJson now wraps arrays/primitives as { result: <value> }.

scripts/smoke-test.ts spawns the built server over stdio, calls each
read-side tool against the live DB and asserts shape — surfaces this
bug class before regressions ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:38:39 +02:00

52 lines
1.5 KiB
TypeScript

import { z } from 'zod'
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import { PermissionDeniedError } from './auth.js'
export function formatZodError(err: z.ZodError): string {
return err.issues
.map((issue) => {
const path = issue.path.length ? issue.path.join('.') : '(root)'
return `${path}: ${issue.message}`
})
.join('; ')
}
export function toolError(message: string): CallToolResult {
return {
content: [{ type: 'text', text: message }],
isError: true,
}
}
export function toolJson(value: unknown): CallToolResult {
const text = JSON.stringify(value, null, 2)
// structuredContent must be a JSON object per the MCP spec — wrap arrays
// and primitives so the SDK's response validator accepts them.
const structured: Record<string, unknown> =
value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: { result: value }
return {
content: [{ type: 'text', text }],
structuredContent: structured,
}
}
export async function withToolErrors<T>(
fn: () => Promise<CallToolResult>,
): Promise<CallToolResult> {
try {
return await fn()
} catch (err) {
if (err instanceof PermissionDeniedError) {
return toolError(`PERMISSION_DENIED: ${err.message}`)
}
if (err instanceof z.ZodError) {
return toolError(`VALIDATION_ERROR: ${formatZodError(err)}`)
}
if (err instanceof Error) {
return toolError(err.message)
}
return toolError(String(err))
}
}