Repository navigation
Log why an MCP server was unavailable, not just that it was - #51
Conversation
connectRemote and connectLocal both capture the failure reason into status.error, but the log site only emitted status.status, so a connect failure reached Loki as `status=failed` with no exception attached. That is the common failure path: over 15 days of Replo production logs it accounted for 109 of 111 occurrences, against 2 for the tools/list path that does log its error. Losing the reason on the frequent one made the whole class undiagnosable. Status is a discriminated union and needs_auth carries no error, so the `in` check narrows to the two variants that do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/mcp/index.ts">
<violation number="1" location="packages/opencode/src/mcp/index.ts:406">
P3: For the `needs_auth` status the log line now carries an `error: undefined` field, which is noise in exactly the log this PR is improving. Spread the field conditionally so it is absent rather than `undefined` when the status has no error.</violation>
<violation number="2" location="packages/opencode/src/mcp/index.ts:406">
P2: The newly logged `status.error` is raw `error.message` from the connect path. For local servers this can embed subprocess/stderr text, and for remote servers it can embed URL or server-returned content, so a server or misconfigured URL containing credentials would now dump them into logs. Mask/sanitize the error string (strip URLs' userinfo, cap length) before logging, or verify none of these transports place credentials in `error.message`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| key, | ||
| type: mcp.type, | ||
| status: status.status, | ||
| error: "error" in status ? status.error : undefined, |
There was a problem hiding this comment.
P2: The newly logged status.error is raw error.message from the connect path. For local servers this can embed subprocess/stderr text, and for remote servers it can embed URL or server-returned content, so a server or misconfigured URL containing credentials would now dump them into logs. Mask/sanitize the error string (strip URLs' userinfo, cap length) before logging, or verify none of these transports place credentials in error.message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/mcp/index.ts, line 406:
<comment>The newly logged `status.error` is raw `error.message` from the connect path. For local servers this can embed subprocess/stderr text, and for remote servers it can embed URL or server-returned content, so a server or misconfigured URL containing credentials would now dump them into logs. Mask/sanitize the error string (strip URLs' userinfo, cap length) before logging, or verify none of these transports place credentials in `error.message`.</comment>
<file context>
@@ -399,7 +399,12 @@ const layer = Layer.effect(
+ key,
+ type: mcp.type,
+ status: status.status,
+ error: "error" in status ? status.error : undefined,
+ })
}
</file context>
| key, | ||
| type: mcp.type, | ||
| status: status.status, | ||
| error: "error" in status ? status.error : undefined, |
There was a problem hiding this comment.
P3: For the needs_auth status the log line now carries an error: undefined field, which is noise in exactly the log this PR is improving. Spread the field conditionally so it is absent rather than undefined when the status has no error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/mcp/index.ts, line 406:
<comment>For the `needs_auth` status the log line now carries an `error: undefined` field, which is noise in exactly the log this PR is improving. Spread the field conditionally so it is absent rather than `undefined` when the status has no error.</comment>
<file context>
@@ -399,7 +399,12 @@ const layer = Layer.effect(
+ key,
+ type: mcp.type,
+ status: status.status,
+ error: "error" in status ? status.error : undefined,
+ })
}
</file context>
| error: "error" in status ? status.error : undefined, | |
| ...("error" in status ? { error: status.error } : {}), |
When a tool server fails to start inside an agent sandbox, our logs currently say only that it failed — not why. This adds the actual error to that log line, so the next time a sandbox comes up missing its tools, whoever investigates can see the reason instead of guessing.
Context
Found while root-causing a production alert in Replo (REPL-32353, customer report USE-2645). Companion fix in the Replo repo: replohq/andytown#28101, which makes the sandbox retry a server whose bring-up failed. This PR is the observability half — it does not change behavior.
The gap
create()has two failure exits, and only one of them logs its reason:tools/listfails after connect →failed to get toolswitherrorserver unavailablewithstatus=failedand nothing elseBoth
connectRemoteandconnectLocalalready capture the reason intostatus.error; the log site just dropped it on the floor.The asymmetry matters because the silent path is the common one. Over 15 days of Replo production logs,
server unavailableaccounted for 109 of 111 occurrences against 2 for the path that does log its error. So in practice this class of failure was undiagnosable — we could see that a sandbox came up with zero tools from a server, and never why.Implementation note
Statusis a discriminated union:failedandneeds_client_registrationcarryerror: string, whileneeds_authdoes not. The"error" in statuscheck narrows to the two variants that have it and types asstring | undefined, so there is no cast and no optional access on a field that may not exist.Testing Done
New test in
test/mcp/lifecycle.test.ts, alongside the existingunavailable remote server is marked failed without tools(same 503Bun.servefixture) and mirroringfailed tool discovery surfaces the underlying error in the status, which is the analogue for the other path. It captures logs with theLogger.make+Logger.layer([capture], { mergeWithExisting: true })pattern already used intest/session/prompt.test.ts.Verified it fails without the source change (
errorcomes backundefined) and passes with it. Fulltest/mcp/suite: 64 pass, 0 fail.tsgo --noEmitonopencode: clean. oxlint: 5 warnings before and after — all pre-existing in untouched code, none added.prettier --checkflagssrc/mcp/index.ts, but it does so on a cleandevtoo; prettier's own output for the changed block is byte-identical to what is committed, so no unrelated lines were reformatted.Risk
Log annotation only — no control flow, no data, no API contract. One thing worth a second look: this puts an error message into a log, and Replo's sandbox-perimeter work explicitly aims to keep credentials out of logs and error bodies. The reason travels via
client.connect(transport); auth material rides inAuthorizationheaders rather than URLs, so these errors surface status codes and transport failures. I believe the risk is low, but it is exactly the invariant that work established, so it deserves review rather than my word.Rollback is a revert — nothing persists.
🤖 Generated with Claude Code
Summary by cubic
Adds the failure reason to the MCP "server unavailable" log line so connect failures are diagnosable rather than a bare
status=failed.Found while root-causing REPL-32353: a sandbox came up with zero tools, and the silent connect-failure path had logged 109 of 111 production occurrences over 15 days, versus 2 for the path that already included the error. This is logging-only — it does not change control flow. The
"error" in statuscheck narrows the discriminated union sinceneeds_authcarries no error field. One thing to verify in review: the error travels viaclient.connect(transport); auth material rides inAuthorizationheaders, so these errors surface status codes and transport failures, not credentials.Written for commit 5917dc7. Summary will update on new commits.