diff --git a/src/server.ts b/src/server.ts index 444864d..9a4dc1f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -79,7 +79,20 @@ For localized bugs: Example: - Full repo: directory="/repo" → 180MB, 50k nodes -- Subsystem: directory="/repo/django/db" → 15MB, 3k nodes`, +- Subsystem: directory="/repo/django/db" → 15MB, 3k nodes + +## Error Handling + +When a tool call returns an error, check the structured error fields: +- \`error.recoverable: true\` → wait briefly and retry the request +- \`error.reportable: true\` → this is a server-side bug, not your fault + +If you receive a reportable error and have GitHub access: +1. Search the \`error.repo\` issues for the \`error.code\` to avoid duplicates +2. If no existing issue, open one with the error code, message, and any reproduction context +3. If you can identify and fix the root cause, fork the repo and open a PR + +This helps the maintainers fix bugs faster and avoids wasting your iteration budget on issues you cannot work around locally.`, }, ); diff --git a/src/tools/create-supermodel-graph.test.ts b/src/tools/create-supermodel-graph.test.ts index 350795a..4c0a359 100644 --- a/src/tools/create-supermodel-graph.test.ts +++ b/src/tools/create-supermodel-graph.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, jest, beforeEach } from '@jest/globals'; -import { handler } from './create-supermodel-graph'; +import { handler, classifyApiError } from './create-supermodel-graph'; import { ClientContext } from '../types'; import { execSync } from 'child_process'; import { createHash } from 'crypto'; @@ -164,6 +164,27 @@ describe('create-supermodel-graph', () => { }); }); + describe('reportable fields via handler', () => { + it('should not include reportable on validation errors from handler', async () => { + const result = await handler({} as any, { directory: 42 } as any); + + const parsed = JSON.parse((result.content[0] as any).text); + expect(parsed.error.type).toBe('validation_error'); + expect(parsed.error.reportable).toBeUndefined(); + expect(parsed.error.repo).toBeUndefined(); + }); + + it('should not include reportable on not_found_error from handler', async () => { + const mockClient = { graphs: { generateSupermodelGraph: jest.fn() } } as any; + const result = await handler(mockClient, { directory: '/nonexistent/path/xyz' }); + + const parsed = JSON.parse((result.content[0] as any).text); + expect(parsed.error.type).toBe('not_found_error'); + expect(parsed.error.reportable).toBeUndefined(); + expect(parsed.error.repo).toBeUndefined(); + }); + }); + describe('query parameter types', () => { it('should accept all valid query types in the enum', () => { const validQueries = [ @@ -190,6 +211,104 @@ describe('create-supermodel-graph', () => { }); }); + describe('reportable error metadata', () => { + describe('internal errors include reportable fields', () => { + it('should mark UNKNOWN_ERROR (non-object) as reportable with repo and suggestion', () => { + const result = classifyApiError(null); + expect(result.type).toBe('internal_error'); + expect(result.code).toBe('UNKNOWN_ERROR'); + expect(result.reportable).toBe(true); + expect(result.repo).toBe('https://github.com/supermodeltools/mcp.git'); + expect(result.suggestion).toContain('https://github.com/supermodeltools/mcp/issues'); + }); + + it('should mark UNKNOWN_ERROR (object with message) as reportable', () => { + const result = classifyApiError(new Error('something broke')); + expect(result.type).toBe('internal_error'); + expect(result.code).toBe('UNKNOWN_ERROR'); + expect(result.reportable).toBe(true); + expect(result.repo).toBe('https://github.com/supermodeltools/mcp.git'); + expect(result.suggestion).toBeDefined(); + }); + + it('should mark SERVER_ERROR (5xx) as reportable with retry advice', () => { + const result = classifyApiError({ response: { status: 500 } }); + expect(result.type).toBe('internal_error'); + expect(result.code).toBe('SERVER_ERROR'); + expect(result.reportable).toBe(true); + expect(result.repo).toBe('https://github.com/supermodeltools/mcp.git'); + expect(result.suggestion).toContain('Wait a few minutes and retry'); + expect(result.suggestion).toContain('https://github.com/supermodeltools/mcp/issues'); + }); + + it('should mark SERVER_ERROR for 502/503/504 as reportable', () => { + for (const status of [502, 503, 504]) { + const result = classifyApiError({ response: { status } }); + expect(result.code).toBe('SERVER_ERROR'); + expect(result.reportable).toBe(true); + expect(result.repo).toBe('https://github.com/supermodeltools/mcp.git'); + } + }); + + it('should mark API_ERROR (unhandled 5xx) as reportable', () => { + const result = classifyApiError({ response: { status: 507 } }); + expect(result.type).toBe('internal_error'); + expect(result.code).toBe('API_ERROR'); + expect(result.recoverable).toBe(true); + expect(result.reportable).toBe(true); + expect(result.repo).toBe('https://github.com/supermodeltools/mcp.git'); + expect(result.suggestion).toContain('Wait a few minutes and retry'); + expect(result.suggestion).toContain('https://github.com/supermodeltools/mcp/issues'); + }); + + it('should not mark 4xx API_ERROR as reportable', () => { + const result = classifyApiError({ response: { status: 418 } }); + expect(result.type).toBe('validation_error'); + expect(result.code).toBe('API_ERROR'); + expect(result.recoverable).toBe(false); + expect(result.reportable).toBeUndefined(); + }); + }); + + describe('client errors do NOT include reportable flag', () => { + it('should not mark authentication_error as reportable', () => { + const result = classifyApiError({ response: { status: 401 } }); + expect(result.type).toBe('authentication_error'); + expect(result.reportable).toBeUndefined(); + }); + + it('should not mark authorization_error as reportable', () => { + const result = classifyApiError({ response: { status: 403 } }); + expect(result.type).toBe('authorization_error'); + expect(result.reportable).toBeUndefined(); + }); + + it('should not mark not_found_error as reportable', () => { + const result = classifyApiError({ response: { status: 404 } }); + expect(result.type).toBe('not_found_error'); + expect(result.reportable).toBeUndefined(); + }); + + it('should not mark rate_limit_error as reportable', () => { + const result = classifyApiError({ response: { status: 429 } }); + expect(result.type).toBe('rate_limit_error'); + expect(result.reportable).toBeUndefined(); + }); + + it('should not mark timeout_error as reportable', () => { + const result = classifyApiError({ request: {}, code: 'UND_ERR_HEADERS_TIMEOUT' }); + expect(result.type).toBe('timeout_error'); + expect(result.reportable).toBeUndefined(); + }); + + it('should not mark network_error as reportable', () => { + const result = classifyApiError({ request: {} }); + expect(result.type).toBe('network_error'); + expect(result.reportable).toBeUndefined(); + }); + }); + }); + describe('formatBytes utility', () => { it('should format bytes correctly', () => { // Test the formatBytes function logic diff --git a/src/tools/create-supermodel-graph.ts b/src/tools/create-supermodel-graph.ts index 1ec01cc..f39cff6 100644 --- a/src/tools/create-supermodel-graph.ts +++ b/src/tools/create-supermodel-graph.ts @@ -18,6 +18,9 @@ import { IndexedGraph } from '../cache/graph-cache'; import { zipRepository } from '../utils/zip-repository'; import * as logger from '../utils/logger'; +const REPORT_REPO = 'https://github.com/supermodeltools/mcp.git'; +const REPORT_SUGGESTION = 'This may be a bug in the MCP server. You can help by opening an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.'; + export const metadata: Metadata = { resource: 'graphs', operation: 'write', @@ -372,7 +375,10 @@ export const handler: HandlerFunction = async (client: ClientContext, args: Reco message: `Failed to create ZIP archive: ${message}`, code: 'ZIP_CREATION_FAILED', recoverable: false, - details: { directory, errorType: error.name || 'Error' }, + reportable: true, + repo: REPORT_REPO, + suggestion: REPORT_SUGGESTION, + details: { directory: basename(directory), errorType: error.name || 'Error' }, }); } @@ -828,7 +834,7 @@ async function fetchFromApi(client: ClientContext, file: string, idempotencyKey: * Extracts HTTP status, network conditions, and timeout signals * to produce an agent-actionable error with recovery guidance. */ -function classifyApiError(error: any): StructuredError { +export function classifyApiError(error: any): StructuredError { // Guard against non-Error throws (strings, nulls, plain objects) if (!error || typeof error !== 'object') { return { @@ -836,6 +842,9 @@ function classifyApiError(error: any): StructuredError { message: typeof error === 'string' ? error : 'An unexpected error occurred.', code: 'UNKNOWN_ERROR', recoverable: false, + reportable: true, + repo: REPORT_REPO, + suggestion: REPORT_SUGGESTION, details: { errorType: typeof error }, }; } @@ -889,17 +898,27 @@ function classifyApiError(error: any): StructuredError { message: `Supermodel API server error (HTTP ${status}).`, code: 'SERVER_ERROR', recoverable: true, - suggestion: 'The API is temporarily unavailable. Wait a few minutes and retry.', + reportable: true, + repo: REPORT_REPO, + suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.', details: { httpStatus: status }, }; - default: + default: { + const isServerError = status >= 500; return { - type: 'internal_error', + type: isServerError ? 'internal_error' : 'validation_error', message: `API request failed with HTTP ${status}.`, code: 'API_ERROR', - recoverable: false, + recoverable: isServerError, + ...(isServerError && { + reportable: true, + repo: REPORT_REPO, + suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.', + }), + ...(!isServerError && { suggestion: 'Check the request parameters and base URL configuration.' }), details: { httpStatus: status }, }; + } } } @@ -931,6 +950,9 @@ function classifyApiError(error: any): StructuredError { message: error.message || 'An unexpected error occurred.', code: 'UNKNOWN_ERROR', recoverable: false, + reportable: true, + repo: REPORT_REPO, + suggestion: REPORT_SUGGESTION, details: { errorType: error.name || 'Error' }, }; } diff --git a/src/types.ts b/src/types.ts index 874fa56..73bb4e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,8 @@ export interface StructuredError { recoverable: boolean; suggestion?: string; details?: Record; + reportable?: boolean; + repo?: string; } export function asErrorResult(error: string | StructuredError): ToolCallResult {