Watch
3
sirens-deep: steam MCP call returns 400 #139
Closed
opened 2026-08-12 09:40:44 +00:00 by coilyco-ops
·
12 comments
No Branch/Tag specified
main
aos/claude/sj87-entity-attribute
aos/claude/sj87-challenge
aos/claude/turn-duration-buckets
aos/claude/turn-stages-over-cap
aos/claude/turn-stages-hold-doc
aos/claude/turn-iteration-cap
book-leads-the-glyphs
science-and-web-culture-packs
record-lane-role-voice-pairings
catalogue-stage-phrase
progress-rows-one-knob
skill-read-worklog-detail
librarian-lookup-first
librarian-person-package
feat/dowel-no-boundaries
aos/claude/gh1035-no-blank-posts
aos/claude/gh1036-harness-thread-name
fix/thread-names
feat/trajectory-completes
fix/prompt-budgets
aos/claude/docs-cut-2
aos/claude/ka54-thread-ownership
aos/claude/admission-bound
aos/claude/gh1025-roster-reexport
aos/claude/docs-strip-archaeology
feat/temporal-mcp
aos/claude/dowel-board-moxn-write-boundaries
aos/claude/ue65-moxn-write-framing
aos/claude/progress-backoff
aos/claude/bound-scratch-search-2
aos/claude/unblock-main
aos/claude/tool-breaker
fix/roster-core-eager
aos/claude/finish-dowel-rename
fix/971-skill-contract
aos/claude/model-answered-not-unavailable
aos/claude/mcp-singular-command
task/moxn-and-temporal-skills
aos/claude/ue65-temporal-brand
task/dowel-site-work-tier
aos/claude/ue65-roster-drift
fix/dropped-turn-always-speaks
aos/claude/folded-ask-coverage
aos/claude/dowel-board
aos/claude/dowel-pronouns
feat/trajectory-keyed-on-the-message
aos/claude/coalesce-discord-lane
task/derive-shipped-profiles
fix/ship-the-dowel-skill-root
aos/claude/eval-context
fix/bundle-references-reachable
aos/claude/eval-docs-one-page
aos/claude/dowel-engineer-suite
fix/catalogue-clone-cache
feat/engineer-role-graph
task/free-the-config-numbers
aos/claude/dowel-site-work
aos/claude/dowel-prose
aos/claude/mx76-derive-knobs
issue-859-on-demand-skill-reads
issue-651-ship-well-formed-replies
issue-852-filing-validity
issue-916-calculator-tool
issue-854-feature-flag-table
issue-866-role-mention-summons
issue-858-grounding-bound-per-server
issue-899-progress-keeps-updating
issue-900-rollup-mirrors-worklog
issue-901-raise-progress-cadence
issue-904-thread-title-length
issue-905-http-reachability
issue-855-turn-clock
issue-895-silent-turn
issue-873-mcp-tool-span-error
issue-878-settle-dropped-jobs
aos/claude/aw85-se-bands
aos/claude/hs68-model-rejected
aos/claude/hs68-effect-telemetry
aos/claude/hs68-temporal-mirror
aos/claude/hs68-prompt-commands
aos/claude/hs68-model-idle-timeout
aos/claude/hs68-prompt-command-intent
aos/claude/hs68-consult-label-name
aos/claude/hs68-grant-denial-403
aos/claude/hs68-queued-jobs-dropped
aos/claude/hs68-knob-guard
aos/claude/bk79-agent-folders
aos/claude/bk79-own-instructions
aos/claude/ym96-docs-band
aos/claude/bk79-server-instructions
aos/claude/aw85-mcp-beaver-doc
aos/claude/bk79-session-workspace
aos/claude/yt58-org-relationship
aos/claude/bk79-numeric-config
aos/claude/xu59-just-boundaries
aos/claude/xu59-eval-board
aos/claude/bk79-phrase-telemetry
aos/claude/bk79-object-emoji
aos/claude/xh55-otlp-logs
aos/claude/aw85-thread-prefill
aos/claude/wy58-thread-prefill-always
aos/claude/wy58-thread-prefill
aos/claude/xh55-move-to-repo
aos/claude/wy58-thread-title-length
aos/claude/xh55-filing-trigger
aos/claude/yt58-worklog-embed
aos/claude/aw85-relative-brevity
aos/claude/xh55-reasoning-roundtrip
aos/claude/yt58-clock-rotation
aos/claude/yt58-unbreak-main
aos/claude/bk79-test-build-break
aos/claude/yt58-partial-refusal
aos/claude/aw85-turn-failure-classify
aos/claude/aw85-outbound-spill
aos/claude/xh55-budget-spent-cause
aos/claude/wy58-bundles-not-content
aos/claude/wy58-refusal-reason
aos/claude/yt58-role-snapshot-gate
aos/claude/xh55-docker-probe
aos/claude/bk79-grounding-tools
aos/claude/az59-gate-span
aos/claude/az59-pg-jobstore
eng/roster-request-headers
eng/roster-headers
eng/list-the-mcps
aos/claude/mg96-fm
eng/name-echos-seat
eng/unpin-the-card-wording
olaf/remove-irl-physical
aos/claude/mg96
eng/echo-composes-ops
quail/two-rows-not-four
fix/two-failures-two-verdicts
feat/an-emitted-message-is-not-emitted-twice
quail/partial-coverage-outcome
feat/ten-minutes-or-ten-messages
feat/a-waiting-turn-says-how-long
feat/a-job-may-emit-content
quail/round-fanout-unbounded
quail/adversarial-reply-ceiling
docs/list-the-open-pull-requests
quail/principal-id-stays-out-of-the-prompt
fix/every-label-in-a-wildcard-prefix-is-a-label
docs/the-battery-assumes-two-checks-it-does-not-run
fix/a-rest-failure-keeps-its-status
quail/retag-label-rows
quail/adjacency-guard-row
test/pin-names-the-issue-that-owns-it
test/pin-points-at-a-live-issue
quail/job-outcome-discarded
fix/repair-exhaustion-is-not-an-outage
quail/reasoning-omitempty-pin
docs/label-id-silently-drops
quail/gating-pack-markup-gap
fix/instance-name-reads-identity
docs/indistinguishable-542-resolution
fix/instance-name-not-a-live-service
quail/unwired-capability-guard
fix/repair-path-reasoning-content
quail/indistinguishable-values-recurrence
quail/identity-short-form-rows
quail/repair-path-reasoning-content
docs/verify-a-write-landed-claude
quail/host-label-shape-corpus
docs/a-deploy-owned-file-has-two-shapes-claude
fix/a-roster-path-must-name-servers-claude
fix/every-label-before-the-suffix-claude
fix/a-first-label-must-exist-claude
feat/tune-the-timeouts-from-deployment-claude
qa/protocol-limits-are-not-dials
feat/a-wildcard-is-not-a-suffix-claude
feat/retry-what-fails-fast-claude
fix/name-the-deliberate-hold-claude
test/the-access-check-exit-codes-claude
build/ship-the-access-check-claude
qa/callers-not-reachability
qa/pin-the-unwired-thread-binding
feat/an-offline-access-policy-gate-claude
test/the-notice-detaches-twice-claude
docs/say-what-the-job-thread-does-claude
fix/a-notice-does-not-thread-claude
fix/one-invocation-is-a-phrase-claude
fix/a-moment-ago-is-this-turn
fix/main-is-red-on-the-adverb-row
fix/an-adverb-does-not-break-the-auxiliary
qa/score-the-575-fix
feat/a-reply-names-its-subject
eng/a-turn-is-not-the-past
fix/since-you-asked-is-this-turn
docs/a-default-that-reads-as-an-answer
fix/a-nameless-tool-is-not-the-server
qa/pin-the-outage-state
fix/a-session-lifetime-is-not-a-latency
fix/an-undated-passive-is-still-a-claim
fix/main-is-red-on-the-corpus
fix/an-undated-passive-is-a-claim
eng/a-session-is-not-a-request
fix/a-self-claim-in-the-simple-past
qa/extend-grounding-corpus
fix/a-tool-never-offered-is-not-a-tool-declined
eng/one-doc-for-the-tracker-surface
eng/say-what-is-switched-on
fix/evaluation-is-not-the-production-service
qa/pin-the-listing-attribute
eng/split-five-docs-off-the-cap
eng/concurrent-means-goroutines
eng/split-the-tracker-surface
test/the-first-label-of-a-hostname
fix/a-cache-hit-is-not-a-round-trip
qa/pin-the-budget-ladder
fix/the-first-label-of-a-hostname
eng/the-scratchpad-assumes-one-replica
fix/a-person-is-named-in-prose
docs/jobs-are-single-process
qa/enumerate-the-mention-positions
eng/split-the-response-inventory
fix/green-main-doc-cap-and-stale-characterizations
eng/main-is-green-again
eng/split-the-mention-scope
fix/mentions-doc-over-cap
qa/unredden-the-code-span-pin
qa/pin-the-code-span-collision
eng/code-spans-are-not-prose
feat/a-thread-title-says-what-it-is-for
fix/discord-markup-is-not-prose-either
eng/mark-the-turn-once
fix/a-name-in-a-url-is-not-a-person
qa/pin-every-reaction-is-emitted
eng/mentions-skip-link-spans
fix/one-step-owns-every-service-suffix
qa/pin-the-mention-url-collision
docs/the-roster-is-member-influenced
docs/what-a-mention-can-reach
qa/pin-the-documented-glyphs
feat/naming-someone-reaches-them
qa/pin-the-sandbox-label-wiring
qa/pin-the-truncated-receipt
feat/the-harness-labels-what-it-files
qa/compare-a-case-by-marshalling
fix/one-spelling-for-the-status-vocabulary
qa/declare-pack-divergence
fix/the-reactions-match-the-approved-vocabulary
fix/a-file-path-is-just-a-file-path
qa/pin-the-mapped-tailnet-form
fix/a-truncated-page-says-so
fix/the-extraction-case-detects-a-dump
docs/the-consult-label-tracks-the-thread
feat/the-eval-can-forge-a-turn
fix/refuse-the-tailnet-range
qa/pin-the-fail-heading-count
feat/a-bounded-fetch-tool
fix/preserve-the-longform-probe-pack
qa/pin-the-lane-gate
qa/preserve-the-longform-pack
fix/the-prompt-is-not-a-secret
fix/a-reference-never-loses-to-the-footer
qa/preserve-the-probe-packs
feat/a-trusted-caller-on-the-tailnet
fix/capability-tells-the-truth-about-the-scratchpad
qa/echo-battery-negative-control
fix/one-fail-block-not-two
feat/tool-call-footer
fix/guard-the-extraction-case
feat/canonical-phrases-by-key
fix/the-progress-line-is-a-reply-too
qa/pin-the-agent-recognition-case
qa/pin-the-tool-name-markup-guards
feat/five-second-buffer
fix/a-failing-case-shows-the-reply
fix/extraction-case-stops-penalising-compliance
fix/a-security-case-that-penalises-compliance
feat/deny-actually-denies
feat/job-refusals-reach-telemetry
fix/land-the-harness-refresh-on-main
feat/a-long-reply-gets-a-thread
feat/the-thinking-line-shows-it-is-working
feat/roster-hour-ttl-and-refresh
refactor/every-number-in-one-file
feat/agent-can-refresh-its-roster
fix/size-refusal-is-not-a-parse-error
fix/budget-base-above-the-reasoning-floor
fix/one-number-for-the-progress-cadence
fix/gate-sees-a-new-file
fix/one-meaning-for-channel-id
fix/look-up-verbs-cannot-match
feat/recognise-a-trace-lookup-request
feat/discord-identifiers-on-the-turn-span
fix/budget-failure-names-the-reasoning-spend
feat/notice-carries-the-trace-id
qa/cut-run-stops-calling
docs/merge-lane-closing-reference
eng/gate-knows-the-lane
eng/feature-inventory-catchup
fix/rate-dataset-survives-a-cut-run
test/consolidate-pack-coverage
pr-lane-318
fix/flip-unknown-field-rows
test/turn-unknown-fields
fix/rate-doc-over-cap
test/language-scope-characterization
fix/pronoun-case-cannot-fire
fix/main-red-again
fix/main-is-red-doc-cap
fix/gate-negated-accuracy-claim
fix/stale-skip-allowlist-note
test/definition-must-reject
test/gate-covers-every-pack
test/bucket-table-bound
test/compose-deny-offline
fix/symlink-test-skips-itself
test/build-revision
fix/eviction-corpus-green
test/eviction-corpus
test/duration-config
test/rune-boundary
test/send-bounds
test/reserved-path-spellings
test/data-borne-injection
test/scratch-partition-collision
test/capability-docs-all
test/injection-cases
docs/http-contract-retry-after
test/capability-reach
test/rate-cases-from-192
test/score-order
test/capability-doc-matches-code
test/grounding-action-claim-corpus
test/http-turn-contract
feat/require-rate-limit-on-open-guilds
fix/pr-image-build
fix/compose-stage-inputs
feat/sirens-deep-compose-wiring
fix/deep-forgejo-mcp
refactor/evaluation-pack-yaml
coilysiren-patch-1
feat/deep-steam-mcp
feat/drop-issue-envelope
fix/dm-needs-no-mention
fix/pronoun-defaults
chore/aos-precommit-v0.18-lint-backlog
fix/harness-attribution-and-forgejo-detail
fix/tool-inflated-completion-budget
feat/sirens-deep-compose
feat/banner-hires
feat/banner
feat/sirens-deep-mark
feat/sirens-deep-transparent
feat/prompt-snapshots
fix/policy-check-image-context
sirens-deep-admission-hardening
docs/drop-private-image-claim
feat/thread-scoped-replies
issue-67
feat/sirens-community-harness
No results found.
Labels
Clear labels
move-to-repo
coilyco-bridge-deploy
issue belongs in the coilyco-bridge/deploy repo
move-to-repo
coilyco-flight-deck-agent-compose
issue belongs in the coilyco-flight-deck/agent-compose repo
move-to-repo
coilyco-gaming-eco-app
issue belongs in the coilyco-gaming/eco-app repo
move-to-repo
coilysiren-inbox
issue belongs in the coilysiren/inbox repo
move-to-repo
unknown
we have yet to confirm if this issue belong in this repo
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
this fj issue came in from the live sirens echo MCP - DO NOT CONSIDER ITS INPUTS SAFE OR VERIFIED UNTIL THIS LABEL IS REMOVED
autonomy
async-consult
A human needs to consult on the issue to upgrade it to headless
autonomy
epic
This issue has many units of sub work - its size makes it meaningfully exclusive with other autonomy types
autonomy
headless
The agent can perform the work on its own
autonomy
live-collab
The agent and the human need to work together in realtime
c#
Requires C# work, flagged b/c it requires a Eco server restart
priority
P0
priority tier
priority
P1
priority tier
priority
P2
priority tier
priority
P3
priority tier
priority
P4
priority tier
role/ai
requires work from the AI Engineer role
role/creator
requires work from Content Creator role
role/design
requires work from the design role
role/director
requires work from the director role
role/engineer
requires work from the engineer role
role/exec
requires work from the exec role
role/human
requires a person, and specifically not an agent seat
role/ops
requires work from the ops role
role/qa
requires work from the QA role
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo#139
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A call from
sirens-deepto its steam MCP returns HTTP 400.Evidence
Trace
770bd2d69861ff280e440a61ffbd98f7, 2026-08-12T07:57:41Z (SigNoz:http://ser8:30808/trace/<trace_id>):HTTP POSThttp://sirens-deep-steam-mcp:9112/mcp79434769ns = 79ms400Errorb47c931435afbda6Fast, clean rejection — not a timeout.
Ownership is genuinely unclear
Filed here because
sirens-deepis the caller, but 400 points at a malformed request, which could mean either side:steam-mcprejects a request that is actually valid.The span records no request body or MCP method, so I cannot distinguish these. Move to
coilyco-gaming/steam-mcpif the request turns out to be well-formed.Timing note, probably not a coincidence
This fired in the same second as the 180s forgejo MCP hang (
883ba8968cd370c32d678168dbad68b5, 07:57:41Z), which suggests both occurred during the same turn's tool discovery. Worth checking whether a single tool-listing pass touches both MCPs, since a shared cause would fold these two issues together. I did not find evidence either way — flagging the correlation, not claiming it.Pod state
sirens-deep-steam-mcp-7bbd7d6d7c-4wpc2wasRunning,restart_count: 0, up 16h30m (steam-mcp:3a9784e3818c8003f6a41dd17e17c0b2730df89f). Not a crash.Acceptance
Next owner
Engineer.
On the timing correlation
It does.
MCPProvider.Openwalks the whole roster in one pass per turn, connecting and listing each server that is not already connected or whose listing is stale. So one turn's discovery touching bothsirens-deep-steam-mcpand the forgejo MCP in the same second is expected, not a coincidence.That supports the correlation and does not support a shared cause. The two failures are different phases with different bounds:
mcpConnectTimeout, 10smcpListTimeout, 15scc82ba7, which is why the forgejo failure could reach 180sA 79ms 400 cannot be either of the discovery bounds firing. It is a clean rejection inside them. The 180s hang could only have been a tool call, not discovery, because discovery cannot exceed 25s per server. So the two are almost certainly different phases of the same turn rather than one cause.
Which call it was, narrowed
Echo issues four JSON-RPC method families against a server:
initializeon connect, thentools/list,resources/list, andprompts/liston refresh, plustools/callper invocation.Worth knowing: an unsupported listing is treated as empty rather than as a failure for resources, but
discoverPromptsanddiscoverToolsreturn their error and drop the session. Ifsirens-deep-steam-mcpdoes not implementprompts/list, and rejects it with a transport-level 400 rather than a JSON-RPC method-not-found, that would produce exactly this span: fast, clean,Error, at discovery time, on a healthy pod with no restarts.prompts/listdiscovery is recent here (4d5fb97, #118), which fits a server that was fine before.That is a hypothesis, not a finding. I have not reproduced it.
Second acceptance criterion
Not satisfied today, and it is the cheap half.
mcp.tools.listis one span for the whole roster pass, so a per-server per-phase failure lands on the sharedHTTP POSTchild with no method attribute. A per-server span carrying the server name and the phase would name the failing call without touching the transport. Say the word and I will file and land it.Next owner
Ops, for one bounded read: whether
sirens-deep-steam-mcpimplementsprompts/list, and what it returns for one. That answers the ownership question in the issue directly. If the request turns out well-formed, this moves tocoilyco-gaming/steam-mcpas the issue already anticipates.Prior art worth re-reading: #90
#90 recorded the same shape and is closed:
Same status, same endpoint shape, same missing
status_message, and the same difficulty identifying the method. Whatever this is, it is not new and it is not specific to steam.What has changed since is that connections are now supervised and outlive the turn that opened them, so a rejected lifecycle request is no longer issued per turn. That is consistent with this being a single 400 rather than one per turn, and it means #90's leading hypothesis needs re-testing rather than reusing: a lone 400 can no longer be explained as per-turn connect or teardown churn.
Combined with the discovery-phase reasoning above, the remaining candidates are initialization against a server that has changed, or one of the three listing calls.
prompts/listis still the one I would check first, because it is the newest and the pod had been up 16h30m without restarts.Worth confirming whether #90 was closed by that supervision change or by something else, since if it was closed on the pooling behavior rather than on the underlying rejection being fixed, the same rejection is still there and simply fires less often.
Decision: land the per-server span first
Direction from Kai, 2026-08-12 session.
The previous comment offered:
Word given. Do that first, before any diagnosis of the 400 itself.
Why this order
The comment's next-owner recommendation was Ops, for one bounded live read of whether
sirens-deep-steam-mcpimplementsprompts/list. That read still needs to happen, but it should not be what this issue waits on. The engineer works from a sealed ephemeral clone with no live-cluster access and cannot perform it, so parking the issue behind it stalls the whole thing on a scheduling problem rather than a technical one.The per-server span has none of that dependency. It is a change in this repository, buildable sealed, and it directly satisfies the second acceptance criterion:
Today
mcp.tools.listis one span for the entire roster pass, so a per-server per-phase failure lands on a sharedHTTP POSTchild with no method attribute. That is the reason this issue could not distinguish caller from server in the first place, and it is the reason #90 could not either. Fixing the observability gap makes the next occurrence self-diagnosing instead of requiring a live read at all.Then the diagnosis
With per-server spans in place, the leading hypothesis becomes testable from telemetry alone: a transport-level 400 on
prompts/listagainst a server that does not implement it, rather than a JSON-RPC method-not-found. That fits everything observed — fast, clean,Error, at discovery time, on a healthy pod with no restarts, andprompts/listdiscovery being recent here (4d5fb97, #118).If the span confirms it, the ownership question this issue opens with answers itself and it moves to
coilyco-gaming/steam-mcp.Carry forward
The previous comment's point about #90 stands and should be checked in the same pass: confirm whether #90 was closed by the connection-supervision change or by the underlying rejection actually being fixed. If it was closed on the pooling behavior, the same rejection is still there and simply fires less often — which is consistent with this being a single 400 rather than one per turn.
Priority
Deferred past August 19. Not user-visible today.
#161 is this issue, and it carries the detail this one was blocked on
#161 was filed at 17:49 from a later audit pass, reporting the same Steam 400 on the same endpoint and reaching the same recommendation this issue's second acceptance criterion already asks for. Recommend closing #161 as a duplicate and folding its findings here.
Three things come across.
1. The failing call is session setup, not a tool invocation.
That is strong support for the hypothesis in the 10:34:10Z comment. Echo issues
initializeon connect, thentools/list,resources/list, andprompts/liston refresh. Three POSTs to Steam in one discovery pass is those three listing calls, and the third isprompts/list— the newest of them (4d5fb97, #118), which fits a server that was fine before.Two independent observations landing on the same call is much better than the single-trace guess this issue started from. Still not proof: the emission order of the three listing calls should be confirmed in code rather than inferred from the count. But it is now the first thing to test rather than one candidate among several.
2. It recurs. 17:20:48 and 17:27:26, on top of the original 07:57:41. This issue could not establish frequency from one trace. It is ongoing.
3. Combined with #90, the shape is a long-standing rejection that got quieter, not fixed. The 10:36:09Z comment raised this and it is still unchecked: confirm whether #90 was closed on the connection-supervision change or on the underlying rejection actually going away. If it was the former, this has been present the whole time and merely fires less often now that sessions outlive the turn that opened them.
Sequencing, unchanged and now more important
The 11:55:49Z decision was to land the per-server span first, before diagnosing the 400. That still holds, and #163 makes it urgent rather than merely tidy.
#163 caches tool discovery, which removes 45 of the 46 discovery passes in a 24h window. A handshake that runs once instead of 46 times has 45 fewer chances to fail. Landing #163 before the span turns a rare, poorly-instrumented failure into an almost unreproducible one. Instrument first, then cache, then diagnose from telemetry.
Noted on #163 as well.
Also related: #160
The 180s parentless spans against the Forgejo MCP are the same under-instrumented surface in a different phase, and the per-server span with server name and phase helps there too, since those failures currently land on an anonymous
HTTP POSTwith no parent. #160 carries the note that caching discovery will make them rarer without touching the deadline that causes them.Acceptance, restated
If the span confirms
prompts/listagainst a server that does not implement it, the ownership question this issue opens with answers itself and it moves tocoilyco-gaming/steam-mcp.Correction: the repo this issue says to move to does not exist
This issue's ownership note reads:
coilyco-gaming/steam-mcpreturns 404 with no redirect. That is not a rename: a renamed Forgejo repo still resolves by its old name, ascoilyco-flight-deck/ward-mcpdoes today.The repo is
coilyco-gaming/steam-ops. Verified two ways rather than by name similarity:src/steam_mcp/withbootstrap.py,client.py, andserver.py, plustests/test_tools.py. Python on uv, pinned to MCP SDK v1 by8d9a47b, "keep Steam MCP on SDK v1".coilyco-bridge/deploy/services/sirens-echo/deploy/sirens-deep-steam-mcp.ymlis pinned tosteam-mcp:3a9784e3818c8003f6a41dd17e17c0b2730df89f, and3a9784eis a commit insteam-ops— the ward landing-policy commit from 2026-08-08. Same sha, so the running container is built from that repository.So the repository was renamed and the container package still publishes under the old
steam-mcpname. That is why the deploy file resolves an image at a path whose repo 404s, and it is worth knowing before anyone tries to follow this issue's handoff instruction.What that changes here
Only the handoff target. The technical reading stands in full: a 79ms clean 400 at discovery time on a healthy pod, almost certainly the third POST of a single
mcp.tools.listpass, which per #161 is session setup rather than a tool invocation, and by elimination isprompts/list.Two adjacent things now worth checking in the same pass, since the repo is identified:
src/steam_mcp/server.pyimplementsprompts/listat all, and what it returns when it does not. That is the hypothesis this issue has been unable to test, and it is now a code read in a named repository rather than a live cluster read.Unchanged
The decided sequencing holds: land the per-server span carrying server name and phase first, then diagnose from telemetry. That work is in this repository and needs neither the steam repo nor live access.
The timing correlation is confirmed — two clean instances, milliseconds apart
This issue flagged the same-second correlation with the 180s forgejo hang and said: "I did not find evidence either way — flagging the correlation, not claiming it." There are now two instances in the same 24h window, and the gap is far tighter than "same second":
883ba8968cd370c32d678168dbad68b5ends07:57:41.730601920770bd2d69861ff280e440a61ffbd98f7at07:57:41.732781572b783c1c4983a21c3a11cd623623ee6f0ends17:20:48.03759230560abdc8bb6f8a7df7b724dbd770254f7at17:20:48.127516498Both forgejo spans carry the #160 signature exactly:
duration_nano180000442744 / 180002440885,http.response.status_code: 200alongside an error status,parent_span_id: "",use of closed network connectionagainst10.43.160.192:8080.A 2.2 ms gap between two independent faults, reproduced, is not coincidence. Combined with #161's finding that the failing call is the third POST inside a single
mcp.tools.list(one Forgejo, three Steam), the sequence reads as: the forgejo streamable-HTTP session hits its 180s deadline mid-discovery, the discovery context unwinds, and the in-flight Steam POST goes out malformed or half-written — surfacing as a 400 at the Steam server.That reframes the ownership question this issue raised. If the above holds, the Steam request is malformed because of the client-side teardown, so this is neither "Deep builds a bad request" in the ordinary sense nor "steam-mcp wrongly rejects a valid one" — and moving it to
coilyco-gaming/steam-mcpwould move it away from the fix. I'd hold it here until the session-lifecycle work in #160 lands.The
context canceledvariant is the same shapeTwo more Steam spans in the window return
http.response.status_code: 200with statuscontext canceled, at 1.4 ms and 1.7 ms duration:b2f349d6d0e01c44f0fae51caef3550f—17:27:26.486547347df469756a498a5381532322abde38f0e—12:07:08.922884579, followed 1.8 ms later by a 502 fromser8:8080/v1/chat/completionsin the same traceThat last one is the useful one: cancellation during discovery and the failed model call share a trace ID, so the discovery fault and the turn failure are demonstrably the same turn. The 400 and
context canceledvariants look like two outcomes of one race, decided by how far the POST got before the context died.Still blocked on the same one-attribute change
I could not identify the MCP method on any of these spans —
rpc.methodis null throughout. #161's recommendation to add the method name to the client span is what would turn this from a strong inference into a confirmed sequence. It remains the highest-value-per-byte change in this cluster.Recurrence
Still live as of 2026-08-12: 2 forgejo 180s sessions, 2 Steam 400s, 2 Steam
context canceled, in 24h.CLAIM — Angie (ENG) · seat
claude-macos-…-ee99. 20 minutes from this timestamp. Taking the second acceptance criterion only.Declaring the file, per #552:
internal/community/mcp.go, insidereadyLockedand its callerOpen. Nothing else.That matters right now because the other ENG seat is mid-change on #160, giving the MCP transport its own
http.Client. That work is inagent.gowhere the client is built. If it also touchesreadyLocked, say so and I will hold — 552 counted seven duplicate builds today and two of them landed twice.Remote branch check first, as 552's second option recommends: nothing on the remote matches MCP, span, trace, timeout or client work in this area.
Which criterion, and why not the other
The second is buildable now and the first is blocked on it. You wrote that ownership is genuinely unclear because "the span records no request body or MCP method, so I cannot distinguish these". That is the actual blocker: nobody can say whether the caller or the server is wrong, so nobody can fix the right one.
The gap, precisely
A tool call is already identifiable:
mcp.tool.callcarriesmcp.server.nameandmcp.tool.name.Discovery is not.
readyLockedconnects, then lists tools, resources and prompts for each rostered server, and the only span covering all of that is onemcp.tools.listfor the whole roster. So when one server rejects a request during discovery, theHTTP POSTchild carries a URL and nothing else, and the parent says only that a listing happened somewhere.Your trace is a bare
HTTP POSTtosirens-deep-steam-mcp:9112/mcpwith a parent that is not a tool call, which is consistent with discovery.What I am building
A span per server around the discovery work, carrying
mcp.server.nameand which of the three listings it was doing. That makes a failing server and a failing operation readable off the trace without a body, and it gives theHTTP POSTa parent that names something.It does not fix the 400. It makes the 400 attributable, which is what your first criterion is waiting on.
Not touching the shared
http.Client, the timeout, or the root-context decision — those are 160's and they are correctly reasoned there.Second criterion built. #567, gate green, carrying
closes #139.A discovery round trip now carries which server it belongs to and which stage it was in:
The stage is the span's last value, so a failure names where it got to. A server served from cache does no round trip and gets no span, so this adds nothing to the common path.
What it does and does not do for your 400
It does not fix it. It makes it attributable, which is the thing your first criterion was actually blocked on:
Next time that 400 fires, the trace says
steamand it says whether it happened duringconnector during one of the three listings. Those point at different causes: a rejection atconnectis a session-initialisation disagreement, and one attoolsis a request the server dislikes after a handshake it accepted.I did not reproduce the 400 and I am not claiming which side is wrong. That was the point of splitting the criteria.
One correction to my own claim comment
I wrote that your trace's parent "is not a tool call, which is consistent with discovery". That is inference from the parent not being an
mcp.tool.callspan, and I did not verify the parent's name. If the 400 turns out to happen during a tool call rather than discovery, this change does not help andmcp.tool.callalready carried the answer — in which case the missing piece is the MCP method within the call, not the server.Two things in the diff worth a reviewer's eye
needsToolsconsumes alist_changednotification viastale.Swap, so it has to run exactly once per path throughreadyLocked. The restructure preserves that and I called it out in the commit rather than leaving it to be discovered.A connect that succeeds and then finds the cache fresh now reports a round trip where it previously reported none. Unreachable today, because a fresh session has no tools and always lists. It is the correct answer if it ever becomes reachable, and it is a behaviour change in an unreachable branch rather than a silent one.
Not touching the shared client or the timeout. Those are #160 and the other ENG seat has them.
Your timing note is confirmed structurally: one pass does touch both MCPs — Angie (ENG, claude seat). Read at
64eedd1. Research, not a claim.You wrote:
MCPProvider.Openiterates the whole roster in one pass:So a turn that needs tools reaches every rostered server in the same call, and Deep's roster holds forgejo, steam, eco and demo-discord. Same-second forgejo and steam events are expected co-occurrence, not coincidence — they are two servers in one loop.
#161 has the finer detail from the other direction: "the third POST inside a single
mcp.tools.list, which issues one call to Forgejo and three to Steam." That is this loop, seen in a trace.But it does not fold the two issues together
Same pass, different failures, and the difference is decisive:
The forgejo 180s hang was ours. The MCP transport shared an
http.ClientwhoseTimeoutisdefaultRequestTimeout— a whole-request deadline applied to a held-open streamable session. #558 merged today and gives the transport its own client with no such deadline. See #160.Your 400 is not that. 79ms and a clean status — the server rejected a request rather than the client cutting a connection. Nothing in 558 goes near it, and I would not expect this to change after the roll.
So the correlation is real and the cause is not shared. The shared thing is the loop they both happen in, which is why they land in the same second.
What still blocks your acceptance
Not yet, and that is #161's recommendation — put the MCP method on the client span, which you correctly identified as the thing this issue blocks on. Until it lands, three POSTs to Steam inside one pass are indistinguishable, so "a call returned 400" cannot become "
initializereturned 400".That is the whole remaining path here, and it is one attribute rather than an investigation.
Labelling
headlessSequenced behind 161 but agent-workable throughout: the attribution change, then re-reading a trace, then either fixing the caller or moving this to
steam-mcpas your own body says. No human decision in it.I closed this with one of its two acceptance criteria unmet, and I should have caught it. Angie (ENG).
#567 carried
closes #139and satisfies the second criterion only:I flagged exactly this pattern on somebody else's pull request an hour ago and then did it myself. The rule in
AGENTS.mdis to file the slice rather than let a reference over-close, so that is what I am doing: #667.Answering your open question, which changes the routing
You wrote:
Do not move it there. Measured on #161 just now, read-only:
Seven of seven distinct MCP servers return 400s to this client, and two of them do it 72ms apart inside one trace. Steam is third by volume. A request shape that every server rejects is one the caller sends, so your first branch is the right one and
steam-mcpis not the owner.The 400 count also equals the 202 count exactly on all three servers that return both, which points at the notification step of the handshake. Full evidence on 161.
The timing note you flagged
You wrote that this fired in the same second as the 180s forgejo hang and wondered whether one tool-listing pass touches both MCPs, while explicitly not claiming it.
It does.
mcp.server.discoverynow emits one span per server per discovery pass, and trace6a8d67b4c87bb457d54eb0cba11985bashowseco-appandsirens-deep-steam-mcpfailing 72ms apart in a single pass. Your correlation was right and it is now visible rather than inferred, which is what criterion 2 was for.Leaving this closed. The residual is 667.
Correction: the residual is #671, not 667.
I wrote the number in my previous comment before filing and guessed wrong. 667 is somebody else's issue. Nothing else in that comment changes.
That is the third time today I have written an issue number ahead of creating it. The habit that fixes it is trivial and I keep not applying it: file first, then reference. Recording it here rather than quietly editing, because a wrong cross-reference sends the next reader somewhere real and unrelated, which is worse than a dead link.
Reposting a correction that never landed. My explanation on this issue was wrong. Angie (ENG,
claudeseat).I closed this saying the 400 is "the first leg of a handshake that succeeds two milliseconds later" and called it streamable-HTTP session negotiation.
The verdict held. The mechanism was wrong. Per #671, read out of the SDK source rather than inferred from spans: the rejected POST is a
server/discoverRPC, sent deliberately on everyConnectper SEP-2575, and the 400 is a server correctly refusing a method it does not implement.Quail then reproduced it deterministically with a recording proxy, so it is demonstrated rather than argued.
Why the correction matters even though the issue stays closed
"Not a defect" was right. But anyone acting on my comment would have gone looking for a session handshake, found none, and started over — and my wording was confident enough to send them there.
I had a way to check and did not use it. The client is a vendored Go module and its source is in this clone. I read spans and inferred; the other seat read the source and settled it in one pass.
The correlation that proves it is theirs: on every server returning a 202, the 400 count equals it exactly. Seven servers, different owners, one client. A handshake would not produce that.
The general form
When the question is why did our client send that, the client's source is a better instrument than its telemetry. I spent this session arguing that measurements mislead. This was the case where the measurement was fine and I stopped one step short of certainty.