Skip to content

Commit 18dd780

Browse files
committed
feat: enhance error handling in tool call and add unit tests for error scenarios
1 parent eda6af3 commit 18dd780

File tree

3 files changed

+487
-16
lines changed

3 files changed

+487
-16
lines changed

src/server.ts

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030

3131
// Import error handling
3232
import {
33-
MethodNotFoundError,
33+
QuipMCPError,
3434
ResourceNotFoundError} from './errors';
3535

3636
// Import logger
@@ -378,17 +378,57 @@ export async function main(): Promise<void> {
378378
server.setRequestHandler(CallToolRequestSchema, async (request) => {
379379
logger.info(`Handling tools/call request for tool: ${request.params.name}`);
380380

381-
if (request.params.name === "quip_read_spreadsheet") {
381+
try {
382+
if (request.params.name === "quip_read_spreadsheet") {
383+
return {
384+
content: await handleQuipReadSpreadsheet(
385+
request.params.arguments || {},
386+
storageInstance!,
387+
options.mock
388+
)
389+
};
390+
} else {
391+
logger.error(`Unknown tool: ${request.params.name}`);
392+
// Return error as content instead of throwing to ensure response is sent
393+
return {
394+
content: [{
395+
type: "text",
396+
text: JSON.stringify({
397+
error: {
398+
code: -32601,
399+
message: `Method not found: ${request.params.name}`
400+
}
401+
})
402+
}]
403+
};
404+
}
405+
} catch (error) {
406+
// Log the error for debugging
407+
const errorMessage = error instanceof Error ? error.message : String(error);
408+
logger.error(`Error in tool call handler for ${request.params.name}: ${errorMessage}`);
409+
410+
// Instead of throwing, return error as content to ensure response is sent
411+
let errorCode = -32603; // Internal error default
412+
let errorMsg = 'Internal server error';
413+
414+
if (error instanceof QuipMCPError) {
415+
errorCode = error.code;
416+
errorMsg = error.message;
417+
} else if (error instanceof Error) {
418+
errorMsg = error.message;
419+
}
420+
382421
return {
383-
content: await handleQuipReadSpreadsheet(
384-
request.params.arguments || {},
385-
storageInstance!,
386-
options.mock
387-
)
422+
content: [{
423+
type: "text",
424+
text: JSON.stringify({
425+
error: {
426+
code: errorCode,
427+
message: errorMsg
428+
}
429+
})
430+
}]
388431
};
389-
} else {
390-
logger.error(`Unknown tool: ${request.params.name}`);
391-
throw new MethodNotFoundError(`Unknown tool: ${request.params.name}`);
392432
}
393433
});
394434

@@ -442,33 +482,78 @@ export async function main(): Promise<void> {
442482
app.post('/mcp', async (req: Request, res: Response) => {
443483
logger.info('Received POST MCP request');
444484

485+
// Set request timeout to prevent hanging connections
486+
const requestTimeout = setTimeout(() => {
487+
if (!res.headersSent) {
488+
logger.error('Request timeout - forcing response');
489+
res.status(408).json({
490+
jsonrpc: '2.0',
491+
error: {
492+
code: -32000,
493+
message: 'Request timeout'
494+
},
495+
id: req.body?.id || null
496+
});
497+
}
498+
}, 30000); // 30 second timeout
499+
445500
try {
446501
// Each request gets its own transport and server instance for stateless mode
447502
const transport = new StreamableHTTPServerTransport({
448503
sessionIdGenerator: undefined, // No sessions in stateless mode
449504
enableJsonResponse: true
450505
});
451506

452-
// Clean up on request close
507+
// Clean up on request close or completion
508+
const cleanup = () => {
509+
clearTimeout(requestTimeout);
510+
transport.close();
511+
};
512+
453513
res.on('close', () => {
454514
logger.info('Request closed');
455-
transport.close();
515+
cleanup();
516+
});
517+
518+
res.on('finish', () => {
519+
logger.info('Request finished');
520+
cleanup();
456521
});
457522

458523
await server.connect(transport);
459524
await transport.handleRequest(req, res, req.body);
525+
526+
// Clear timeout on successful completion
527+
clearTimeout(requestTimeout);
528+
460529
} catch (error) {
530+
// Clear timeout on error
531+
clearTimeout(requestTimeout);
532+
461533
const errorMessage = error instanceof Error ? error.message : String(error);
462534
logger.error('Error handling MCP request:', { error: errorMessage });
463535

464536
if (!res.headersSent) {
537+
// Determine appropriate error code and message based on error type
538+
let errorCode = -32603; // Internal error default
539+
let errorMsg = 'Internal server error';
540+
541+
// Handle specific error types
542+
if (error instanceof Error) {
543+
const err = error as any;
544+
if (err.code && typeof err.code === 'number') {
545+
errorCode = err.code;
546+
errorMsg = error.message;
547+
}
548+
}
549+
465550
res.status(500).json({
466551
jsonrpc: '2.0',
467552
error: {
468-
code: -32603,
469-
message: 'Internal server error'
553+
code: errorCode,
554+
message: errorMsg
470555
},
471-
id: null
556+
id: req.body?.id || null
472557
});
473558
}
474559
}

0 commit comments

Comments
 (0)