Repository navigation
Harden kcap mcp sessions dispatch: guard invalid URL + catch per-request - #226
Conversation
Follow-up to the sessions lazy-auth change (already on main). With the client created lazily, a scheme-less server_url would reach EnsureAbsolute inside the auth-client factory and hard-exit the process (Environment.Exit(2)) mid-request; and any unexpected client-creation/token failure would bubble out of the stdio loop with no JSON-RPC response. Validate the server_url shape once at startup with IsAcceptableUrl (pure local check), and route tools/call through a guarded dispatcher that returns a JSON-RPC tool error for an unusable URL or any unexpected exception. Server keeps serving subsequent requests. Completes the uniform hardening across all three auto-registered MCP servers (flows in #217, review in #224, sessions here). Test: Tool_call_with_invalid_server_url_returns_error_and_server_survives. Sessions integration suite (8) green; no IL3050/IL2026 AOT warnings. Closes #225. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR Summary by QodoHarden MCP sessions server: validate server_url and guard tools/call
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
Context used 1.
|
| var clientLazy = new Lazy<Task<HttpClient>>(() => HttpClientExtensions.CreateAuthenticatedClientAsync(baseUrl)); | ||
|
|
||
| // Validate the server_url shape once, locally (pure string check — no network, token, | ||
| // or stderr). Used to fail gracefully instead of hard-exiting mid-request (below). | ||
| var urlOk = HttpClientExtensions.IsAcceptableUrl(baseUrl); | ||
|
|
||
| // Guarded tool dispatch: never let the stdio JSON-RPC loop die on one bad request. An | ||
| // unusable server_url would otherwise reach EnsureAbsolute inside the lazy auth-client | ||
| // factory, which hard-exits the process (Environment.Exit(2)) mid-request; and an | ||
| // unexpected client-creation/token failure would bubble out of the loop. Return a | ||
| // JSON-RPC tool error in both cases so the server keeps serving. | ||
| async Task<string> DispatchToolCallAsync(JsonNode callId, JsonObject callRequest) { | ||
| if (!urlOk) | ||
| return BuildToolResult(callId, HttpClientExtensions.SchemeMissingHint, isError: true); | ||
|
|
||
| try { | ||
| return await HandleToolCallAsync(callId, callRequest, await clientLazy.Value, baseUrl, cwdRepoHash); | ||
| } catch (Exception ex) { | ||
| return BuildToolResult(callId, $"Error: {ex.Message}", isError: true); | ||
| } |
There was a problem hiding this comment.
2. Faulted lazy client sticks 🐞 Bug ☼ Reliability
If the first lazy authenticated-client creation throws, the cached Lazy<Task<HttpClient>> remains faulted and every later tools/call will continue failing for the lifetime of the process. This undermines the goal of “server keeps serving” after transient failures (e.g., temporary IO issues reading tokens).
Agent Prompt
### Issue description
The MCP server uses `Lazy<Task<HttpClient>>` for on-demand client creation. If the factory throws once, the faulted task is reused on every subsequent access, causing persistent per-request failures even if the underlying problem was transient.
### Issue Context
Client creation can throw due to real IO/permission faults in token/profile loading (these are intentionally not swallowed). After this PR, those exceptions are caught and converted into tool errors, but the server cannot recover because the lazy remains faulted.
### Fix
Replace the `Lazy<Task<HttpClient>>` with a retryable pattern:
- Keep a nullable `HttpClient? client` (or `Task<HttpClient>? clientTask`) and create it on-demand inside `DispatchToolCallAsync`.
- If creation fails, do **not** cache the failure; return an error and allow the next tools/call to attempt creation again.
- Preserve existing disposal behavior: dispose the client at shutdown if it was successfully created.
(Concurrency note: the stdio loop processes requests serially, so a simple nullable field is sufficient; no heavy locking required.)
### Fix Focus Areas
- src/Capacitor.Cli/Commands/McpSessionsServer.cs[19-46]
- src/Capacitor.Cli.Core/Auth/TokenStore.cs[55-76]
- src/Capacitor.Cli.Core/Auth/TokenStore.cs[225-252]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…x.Message leak Two findings on the guarded dispatcher: 1. Reliability: a `Lazy<Task<HttpClient>>` caches a faulted task, so a single transient client-creation failure would make every later tools/call re-throw for the rest of the session — the guard turned a would-be crash (which the host restarts and retries) into a permanently-erroring server. Create the client on demand into a nullable field instead: a failed attempt leaves it null and the next call retries. Safe without locking — the stdio loop handles one request at a time. 2. Security: the catch-all returned `ex.Message` to the MCP client, which could forward local details (e.g. file paths from IO exceptions). It's a safety net for *unexpected* exceptions (expected ones are already handled with useful messages inside HandleToolCallAsync), so log the full exception to stderr and return a generic tool error instead. Sessions integration suite (8) green; no IL3050/IL2026 AOT warnings.
…Message leak Mirror the sessions fixes (#226) in the flows dispatcher: 1. Reliability: replace Lazy<Task<HttpClient>> with an on-demand nullable client so a transient creation failure leaves it null and the next tools/call retries, instead of a faulted task sticking for the session. The AI-1061 InfiniteTimeSpan timeout is applied on creation. 2. Security: the catch-all logs the full exception to stderr and returns a generic tool error instead of forwarding ex.Message to the client. Flows integration suite (18) green; no IL3050/IL2026 AOT warnings.
|
Thanks @qodo-code-review — both fixed in c806604. 1. Faulted lazy client sticks (Reliability) — fixed. Good catch: a 2. Exception text disclosure (Security) — fixed. The catch-all now logs the full exception to Applied the same two fixes to the sibling servers for consistency: flows in #217, review in #224. Sessions integration suite (8) green; no IL3050/IL2026 AOT warnings. |
Thanks — this looks good.
Nice touch applying the same pattern to the sibling servers too. The green integration run and AOT-warning check are reassuring as well. |
…uash-merge (#227) #217 (flows) and #224 (review) were squash-merged at tips that predated their final commits, so main ended up missing work that CI had passed on those PRs: - flows: has only the bare lazy client — missing the startup server_url guard, the per-request try/catch, the on-demand-client-with-retry (Qodo #226), the generic error message, and the invalid-URL integration test. - review: has the guard but still uses Lazy<Task> + returns ex.Message — missing the on-demand-client-with-retry and generic error message. sessions (#226) landed complete. This brings flows and review to the same final state (byte-identical to the merged sessions dispatch pattern): on-demand nullable client so a transient creation failure retries instead of a faulted task sticking; catch-all logs to stderr and returns a generic tool error rather than ex.Message; startup server_url validation so a bad URL yields a JSON-RPC error instead of Environment.Exit mid-request. Restored the final McpFlowsServer.cs / McpReviewServer.cs and the flows invalid-URL integration test from the (still-present) branch commits cd69ef3 and d6b5a9e. Flows integration (18), review integration (4), full unit (2028) green; no IL3050/IL2026 AOT warnings.
Problem
The three auto-registered MCP servers now create the authenticated
HttpClientlazily on firsttools/call. Two dispatch-path gaps (raised by Qodo on #224):server_urlhard-exits mid-request — reachesEnsureAbsolute→Environment.Exit(2)inside the lazy auth-client factory (uncatchable), terminating the process on firsttools/callinstead of returning an error.tools/callarm has no surroundingcatch, so an unexpected failure bubbles out and the server closes stdout without a JSON-RPC response.Change (sessions — the third server)
server_urlonce at startup via the pure, localIsAcceptableUrl(no network/token/stderr).tools/callthrough a guarded dispatcher returning a JSON-RPC tool error for an unusable URL or any unexpected exception; the server keeps serving.initialize/tools/liststay local-only.Tool_call_with_invalid_server_url_returns_error_and_server_survives.This is the same hardening applied to flows (#217) and review (#224) — landing per-server since each one's lazy-client change lives in a different place; sessions' lazy client is already on
main, so this is standalone.Verification
dotnet buildclean;dotnet publish -c Release— no IL3050/IL2026 AOT warnings.Closes #225.
🤖 Generated with Claude Code