Add support for steam-mcp #52

Closed
opened 2026-08-12 11:25:22 +00:00 by coilysiren · 2 comments
Owner
https://forgejo.coilysiren.me/coilyco-gaming/steam-ops
Member

Picked this up in a sweep of the open queue. It needs a decision from you before code, because "add support" has two readings with very different work behind them - and I would rather ask than build the wrong one.

Reading A - proxy the existing server. serve-upstream already wraps any streamable-HTTP MCP with a name allowlist, and steam-mcp is exactly that. Nothing to build; it is a values file in deploy. With a328d5e it can also carry --pin to bound arguments.

Reading B - replace the bespoke server with a guardfile, so steam-ops stops shipping its own Go and becomes one more .mcp.kdl on the generic runtime. That is the product thesis, and I assume it is what you meant.

Feasibility for B, from the deploy README's description of the source: partial, and the split is clean.

  • Steam Web API - expressible today. It authenticates with a key= query parameter, which is exactly auth query-param, and steamid64 is an ordinary path or query field. This part is a straightforward guardfile.
  • Public storefront - expressible today. No credential at all.
  • Authenticated client / PICS plane - not expressible, and not close. That is a Steam client protocol, not HTTP REST. This runtime maps a request to an HTTP upstream and returns the response; it has no way to speak a binary session protocol. Nothing short of a sidecar could cover it - which, since #59 landed, is at least a real option: keep a small PICS-speaking process in-pod and wrap its local HTTP surface.

So B is "most of it, plus a decision about PICS", not a clean lift-and-shift.

Related finding worth carrying across all three of these issues (#50, #51, #52): the guardfile auth block resolves a static value - env, file, or literal. It cannot perform a login or token exchange. That is what makes Steam easy (a fixed API key) and Bluesky hard (an app password exchanged for a session token). Steam is the tractable one of the three.

Which reading did you intend, and do you want the Web API plus storefront half as a guardfile with PICS left in the bespoke server?

Picked this up in a sweep of the open queue. It needs a decision from you before code, because "add support" has two readings with very different work behind them - and I would rather ask than build the wrong one. **Reading A - proxy the existing server.** `serve-upstream` already wraps any streamable-HTTP MCP with a name allowlist, and steam-mcp is exactly that. Nothing to build; it is a values file in deploy. With a328d5e it can also carry `--pin` to bound arguments. **Reading B - replace the bespoke server with a guardfile**, so `steam-ops` stops shipping its own Go and becomes one more `.mcp.kdl` on the generic runtime. That is the product thesis, and I assume it is what you meant. **Feasibility for B, from the deploy README's description of the source:** partial, and the split is clean. * **Steam Web API** - expressible today. It authenticates with a `key=` query parameter, which is exactly `auth query-param`, and `steamid64` is an ordinary path or query field. This part is a straightforward guardfile. * **Public storefront** - expressible today. No credential at all. * **Authenticated client / PICS plane** - **not expressible, and not close.** That is a Steam client protocol, not HTTP REST. This runtime maps a request to an HTTP upstream and returns the response; it has no way to speak a binary session protocol. Nothing short of a sidecar could cover it - which, since #59 landed, is at least a real option: keep a small PICS-speaking process in-pod and wrap its local HTTP surface. So B is "most of it, plus a decision about PICS", not a clean lift-and-shift. **Related finding worth carrying across all three of these issues** (#50, #51, #52): the guardfile `auth` block resolves a **static** value - `env`, `file`, or `literal`. It cannot perform a login or token exchange. That is what makes Steam easy (a fixed API key) and Bluesky hard (an app password exchanged for a session token). Steam is the tractable one of the three. Which reading did you intend, and do you want the Web API plus storefront half as a guardfile with PICS left in the bespoke server?
Member

Reading B confirmed by Kai. Steam is done — landed in d262653.

Two guardfiles, because the Web API and storefront are different hosts and one guardfile has one base-url. That split turns out to be right on its own terms: different credentials, different risk, different rate profile.

  • examples/steam-web-api.mcp.kdlget_owned_games, get_recently_played
  • examples/steam-storefront.mcp.kdlget_store_app_details, get_store_search_results

Delivering this safely needed one runtime capability first. set writes fixed body values only, so a GET carrying its scope in the query string had no server-side way to fix it. Without that, steamid would have to be a declared caller field — turning "Kai's library" into "any steamid64's library". That is the same hole as #56, in the mode #56 did not cover. So:

pin "get_owned_games" {
    query "steamid" env "STEAM_STEAMID64"
    query "include_appinfo" literal "1"
}

Resolved at call time from env/file/literal, like auth. The pinned name is absent from the tool schema and splitArgs drops anything unschemed, so a caller can neither supply nor override it — tested by calling with a competing steamid and asserting the pinned value still reaches the upstream. No umbra change needed: opcore passes undeclared query names through.

Four of six tools. get_account_licenses and get_pics_product_info ride Steam's authenticated client plane (PICS, refresh-token first), a binary session protocol this HTTP runtime cannot speak. They ship as withhold stubs, so the surface says so instead of leaving an agent to infer it from a hole. Moving them behind a PICS-speaking sidecar is possible now that #59 landed, but it means writing that process — not obviously better than leaving those two in the bespoke server.

Two migration details found by building it rather than reasoning about it:

  • Tool names are preserved exactly. verb_resource keeps underscores, so can get owned_games mints get_owned_games. No consumer sees a rename — which I had assumed would be a cost and is not.
  • One parameter must change. query is a reserved opcore engine flag (with dry-run, output, body-file) and fails closed, so the storefront search input is term rather than query. That is the only caller-visible difference in the whole migration.

The deploy-side work is a values file per server plus STEAM_API_KEY and STEAM_STEAMID64 in SSM. ward exec lint-examples lints both, and every other committed example, on demand.

Closing. The two client-plane tools are recorded above rather than left implicit; reopen or file fresh if you want the sidecar.

Reading B confirmed by Kai. **Steam is done** — landed in d262653. Two guardfiles, because the Web API and storefront are different hosts and one guardfile has one `base-url`. That split turns out to be right on its own terms: different credentials, different risk, different rate profile. * `examples/steam-web-api.mcp.kdl` — `get_owned_games`, `get_recently_played` * `examples/steam-storefront.mcp.kdl` — `get_store_app_details`, `get_store_search_results` **Delivering this safely needed one runtime capability first.** `set` writes fixed *body* values only, so a GET carrying its scope in the query string had no server-side way to fix it. Without that, `steamid` would have to be a declared caller field — turning "Kai's library" into "any steamid64's library". That is the same hole as #56, in the mode #56 did not cover. So: ```kdl pin "get_owned_games" { query "steamid" env "STEAM_STEAMID64" query "include_appinfo" literal "1" } ``` Resolved at call time from `env`/`file`/`literal`, like `auth`. The pinned name is absent from the tool schema and `splitArgs` drops anything unschemed, so a caller can neither supply nor override it — tested by calling with a competing steamid and asserting the pinned value still reaches the upstream. No umbra change needed: opcore passes undeclared query names through. **Four of six tools.** `get_account_licenses` and `get_pics_product_info` ride Steam's authenticated client plane (PICS, refresh-token first), a binary session protocol this HTTP runtime cannot speak. They ship as `withhold` stubs, so the surface says so instead of leaving an agent to infer it from a hole. Moving them behind a PICS-speaking sidecar is possible now that #59 landed, but it means writing that process — not obviously better than leaving those two in the bespoke server. **Two migration details found by building it rather than reasoning about it:** * **Tool names are preserved exactly.** `verb_resource` keeps underscores, so `can get owned_games` mints `get_owned_games`. No consumer sees a rename — which I had assumed would be a cost and is not. * **One parameter must change.** `query` is a reserved opcore engine flag (with `dry-run`, `output`, `body-file`) and fails closed, so the storefront search input is `term` rather than `query`. That is the only caller-visible difference in the whole migration. The deploy-side work is a values file per server plus `STEAM_API_KEY` and `STEAM_STEAMID64` in SSM. `ward exec lint-examples` lints both, and every other committed example, on demand. Closing. The two client-plane tools are recorded above rather than left implicit; reopen or file fresh if you want the sidecar.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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-flight-deck/mcp-beaver#52
No description provided.