-
Notifications
You must be signed in to change notification settings - Fork 10
fix(mcp-server): add diagnostic logging for 500 errors #1456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
16f444f
b1b5194
6f280f9
81539e9
39c832a
2a4d1c9
40dc348
4c8531a
4af5f0d
ffc0dd1
a51c795
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,21 @@ const defaultLogger: Logger = (level, message) => { | |
| getDefaultLogFn(level)(message); | ||
| }; | ||
|
|
||
| function logLevelForStatus(statusCode: number): LogLevel { | ||
| if (statusCode >= 500) return 'Error'; | ||
| if (statusCode >= 400) return 'Warn'; | ||
|
|
||
| return 'Info'; | ||
| } | ||
|
|
||
| function logErrorWithStack(logger: Logger, message: string, err: Error): void { | ||
| logger('Error', message); | ||
|
|
||
| if (err.stack) { | ||
| logger('Error', `Stack: ${err.stack}`); | ||
| } | ||
| } | ||
|
|
||
| /** Fields that are safe to log for each tool (non-sensitive data) */ | ||
| const SAFE_ARGUMENTS_FOR_LOGGING: Record<string, string[]> = { | ||
| list: ['collectionName'], | ||
|
|
@@ -234,7 +249,8 @@ export default class ForestMCPServer { | |
| * Logs the request, intercepts the response for error logging, and delegates to the transport. | ||
| */ | ||
| private async handleMcpRequest(req: express.Request, res: express.Response): Promise<void> { | ||
| this.logger('Info', `Incoming ${req.method} ${req.path}`); | ||
| const rpcMethod = req.body?.method || 'unknown'; | ||
| this.logger('Info', `Incoming ${req.method} ${req.path} [${rpcMethod}]`); | ||
|
|
||
| if (!this.mcpTransport) { | ||
| throw new Error('MCP transport not initialized'); | ||
|
|
@@ -328,17 +344,14 @@ export default class ForestMCPServer { | |
| app.use((req, res, next) => { | ||
| const startTime = Date.now(); | ||
|
|
||
| // Capture the original end method to log after response is sent | ||
| const originalEnd = res.end.bind(res); | ||
| res.end = ((chunk?: unknown, encoding?: BufferEncoding | (() => void)) => { | ||
| res.on('finish', () => { | ||
| const duration = Date.now() - startTime; | ||
| const level = logLevelForStatus(res.statusCode); | ||
| this.logger( | ||
| 'Info', | ||
| level, | ||
| `[${res.statusCode}] ${req.method} ${req.baseUrl || req.path} - ${duration}ms`, | ||
| ); | ||
|
|
||
| return originalEnd(chunk, encoding as BufferEncoding); | ||
| }) as typeof res.end; | ||
| }); | ||
|
|
||
| next(); | ||
| }); | ||
|
|
@@ -373,30 +386,45 @@ export default class ForestMCPServer { | |
| }), | ||
| (req, res) => { | ||
| this.handleMcpRequest(req, res).catch(error => { | ||
| this.logger('Error', `MCP Error: ${error}`); | ||
| const mcpMethod = req.body?.method || 'unknown'; | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| logErrorWithStack( | ||
| this.logger, | ||
| `MCP Error on method '${mcpMethod}': ${err.message || error}`, | ||
| err, | ||
| ); | ||
|
|
||
| if (!res.headersSent) { | ||
| res.status(500).json({ | ||
| jsonrpc: '2.0', | ||
| error: { | ||
| code: -32603, | ||
| message: (error as Error)?.message || 'Internal server error', | ||
| message: err.message || 'Internal server error', | ||
| }, | ||
| id: null, | ||
| }); | ||
| } else { | ||
| this.logger( | ||
| 'Warn', | ||
| `Cannot send error response (headers already sent) for method '${mcpMethod}'`, | ||
| ); | ||
|
Comment on lines
+389
to
+410
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why don't we use here the error handler defined below ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't use the global error handler here because |
||
| } | ||
| }); | ||
| }, | ||
| ); | ||
|
|
||
| // Global error handler to catch any unhandled errors | ||
| // Express requires all 4 parameters to recognize this as an error handler | ||
| // Capture logger for use in error handler (arrow function would lose context) | ||
| // Capture logger reference so the error handler closure does not depend on 'this' | ||
| const { logger } = this; | ||
| app.use( | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| (err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { | ||
| logger('Error', `Unhandled error: ${err.message}`); | ||
| (err: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => { | ||
| logErrorWithStack( | ||
| logger, | ||
| `Unhandled error on ${req.method} ${req.path}: ${err.message}`, | ||
| err, | ||
| ); | ||
|
|
||
| if (!res.headersSent) { | ||
| res.status(500).json({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why taking an error and a message ? Message can be retrieved from the error 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
messageparam adds context thaterr.messagealone doesn't have — e.g."MCP Error on method 'tools/call': <err.message>"or"Unhandled error on POST /mcp: <err.message>". The helper then appends the stack trace on a separate log line. So it'scontext + error messageon line 1,stackon line 2.