Summary
McpServer.ProcessLineAsync (lines 80-124) dispatches each JSON-RPC request to a tool handler with no per-request timeout or CancellationToken. A malformed query, a pathological FTS5 expression that triggers a runaway scan, or an index in a degraded state can hang inside a tool handler indefinitely. Because the stdio loop is single-threaded, that one stuck handler also blocks every subsequent request from the same client until the server is killed.
Where
src/CodeIndex/Mcp/McpServer.cs:80-124 (ProcessLineAsync dispatch)
src/CodeIndex/Mcp/McpToolHandlers.cs (handlers don't accept CancellationToken)
Suggested approach
(1) Plumb a CancellationToken through every handler signature; honor it inside long-running query loops. (2) In ProcessLineAsync, wrap the handler invocation in await Task.WhenAny(handlerTask, Task.Delay(timeout, ct)) (or task.WaitAsync(timeout, ct) on .NET 6+) with a configurable per-request timeout (default 60s, configurable via env or initialize params). (3) Return JSON-RPC -32603 (Internal error) with data: { reason: "timeout", elapsed_ms: ... } on timeout. (4) Coordinate with #1567 (shutdown/cancellation propagation) so the same token also fires on server shutdown. (5) Add a regression test: a handler that sleeps longer than the timeout returns a structured timeout error.
Summary
McpServer.ProcessLineAsync(lines 80-124) dispatches each JSON-RPC request to a tool handler with no per-request timeout orCancellationToken. A malformed query, a pathological FTS5 expression that triggers a runaway scan, or an index in a degraded state can hang inside a tool handler indefinitely. Because the stdio loop is single-threaded, that one stuck handler also blocks every subsequent request from the same client until the server is killed.Where
src/CodeIndex/Mcp/McpServer.cs:80-124(ProcessLineAsync dispatch)src/CodeIndex/Mcp/McpToolHandlers.cs(handlers don't accept CancellationToken)Suggested approach
(1) Plumb a
CancellationTokenthrough every handler signature; honor it inside long-running query loops. (2) InProcessLineAsync, wrap the handler invocation inawait Task.WhenAny(handlerTask, Task.Delay(timeout, ct))(ortask.WaitAsync(timeout, ct)on .NET 6+) with a configurable per-request timeout (default 60s, configurable via env or initialize params). (3) Return JSON-RPC -32603 (Internal error) withdata: { reason: "timeout", elapsed_ms: ... }on timeout. (4) Coordinate with #1567 (shutdown/cancellation propagation) so the same token also fires on server shutdown. (5) Add a regression test: a handler that sleeps longer than the timeout returns a structured timeout error.