live-audit
Skill4 starsChanged 28 days agoMITView on GitHub
Audit steam-games-mcp — build/test/lint gate, live MCP tool edge-case sweep (input validation, SteamID64/vanity/appid edge cases, key-gating), and source-level code review. Use when asked to test/audit the published or just-fixed steam-games-mcp package, hunt for bugs/edge cases, or repeat "the same kind of testing as before.
- Reads credentials
We split this skill into the rules it gives an agent
10 rules. 1 is common, 1 is distinctive, and 8 are very specific to this project.
1
Common
Most of the 3 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 50
If package.json's version matches the npm-published version, live-testing the running tools _is_ testing the published package.
- Common: line 69
All green is a baseline, not proof of correctness — it only confirms nothing already-covered regressed.
- Very specific: line 83
Only the tests that explicitly pass { versionNegotiation: { mode: 'auto' } } ever exercise the modern (2026-07-28) era, so any wire-level …
- Very specific: line 93
Don't try to run the whole unit suite (the ~240+ helpers.ts-based tests) under both eras — confirmed by direct test: a bare …
- Distinctive: line 224
Check the actual response size/token count for the largest realistic case (e.g.
- Very specific: line 264
Live prompt testing: run the prompt-check skill against every prompt in src/tools/prompts.ts — a static read comparing prompt text against …
- Very specific: line 332
Check every call site that takes a user-controlled id/appid for the same "does a 400/404/5xx get normalized, or does raw upstream body leak …
- Very specific: line 447
Run the docs-consistency-check skill.
- Very specific: line 457
If asked to fix: implement the smallest correct change, add/extend a test in the matching src/__tests__/*.test.ts (mirror the existing …
- Very specific: line 504
When the same commit writes both the code and its .describe(), a mismatch reads as correct to its author — get_recently_played shipped …
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
live-audit — steam-games-mcp health check + edge-case hunt
Repo-specific playbook, for any agent/model working on this repo (not tied to
a particular harness — see AGENTS.md's own agent-agnostic framing). Use it
when asked to test/audit the published or just-fixed steam-games-mcp package,
hunt for bugs/edge cases, or repeat "the same kind of testing as before."
Sibling repos (tmdb-mcp, mal-mcp, anilist-mcp-server) keep their own
live-audit/SKILL.md — when either this file or a sibling's improves, sync
the useful parts both ways rather than letting them drift.
Goal: find real bugs/inaccuracies in the live tool behavior (against the real
Steam Storefront + Web APIs) and in the source, then fix what's found. Read
AGENTS.md first if it's not already in context — every fix must follow its
conventions (guard()/never-throw, schema-first format/*.schemas.ts,
keyless-vs-keyed tool gating, commit author/no-Co-Authored-By, etc.).
This assumes the server is already reachable as an MCP connection in your
current session (e.g. as mcp__steam__* tools in Claude Code). If it isn't
connected, connect it first rather than skipping straight to step 1.
Unlike the anilist/mal siblings, this server has no OAuth login and no
mutation tools — every tool is a read against public Steam data. The
per-call risk here isn't "did I just modify a real account," it's "did I just
call a key-gated tool without a key," "did I treat a private profile's
default response as a bug," or "did I burn a real person's SteamID64 in a
committed test fixture." Read ## 2 before live-calling anything.
Contents
- Confirm "published"/"fixed" actually means what you think it means
- Static pass first (cheap, catches regressions before you burn API calls)
- Safety rules for live testing (read before calling anything)
- Live edge-case sweep
- Source-level code review
- Docs/metadata consistency
- Report, then fix only what's confirmed
- Commit + changelog, if asked
0. Confirm "published"/"fixed" actually means what you think it means
node -p "require('./package.json').version"; npm view steam-games-mcp version; git log --oneline -5If package.json's version matches the npm-published version, live-testing
the running tools is testing the published package. If you've since made
local fixes, remember the running MCP server is a separate process from
your edits — stdio servers don't hot-reload. Ask for a restart before
trusting a live call against fixed code, and state plainly whether findings
apply to the published package or to fixed-but-unreleased/unrestarted code.
1. Static pass first (cheap, catches regressions before you burn API calls)
npm run build && npm test && npm run lint && npm run format:checkOptionally npm run check:api too — it's a live upstream health-check
(Storefront + keyless Web API always run; key-gated Web API checks only run
if STEAM_API_KEY is exported in your shell). A failure there means an
upstream shape has drifted, which is exactly the kind of thing this audit is
hunting for — treat it as a finding, not noise to route around.
All green is a baseline, not proof of correctness — it only confirms
nothing already-covered regressed. It says nothing about whether the
interesting logic (error/exception branches especially) is covered at all.
npm run test:coverage (~80% gate) measures lines executed, not whether the
assertions on those lines are meaningful. When reviewing or writing tests as
part of this audit, ask: does a test exist that deliberately triggers this
error path (a private profile, a malformed SteamID64, a delisted appid), and
does it assert on the specific resulting message/shape (not just
"isError: true")?
Also ask the same question about protocol era, not just error paths:
src/__tests__/e2e.test.ts's default connections (helpers.ts's
connectServer(), and the plain new Client(...) in e2e's first test) all
negotiate the legacy (2025) era — versionNegotiation defaults to 'legacy'.
Only the tests that explicitly pass { versionNegotiation: { mode: 'auto' } }
ever exercise the modern (2026-07-28) era, so any wire-level behavior that
differs by era (serverInfo stamping, cacheHints, a future
elicitation/sampling input_required path) needs its own modern-era test —
the legacy-era suite passing green proves nothing about it. Confirmed
concretely: cacheHints (added for tools/list/prompts/list/
server/discover) had to be verified with a dedicated modern-era connection
and a spy ResponseCacheStore, since the legacy-era suite would stay green
whether or not it worked at all.
Don't try to run the whole unit suite (the ~240+ helpers.ts-based tests)
under both eras — confirmed by direct test: a bare McpServer.connect() over
InMemoryTransport (what every one of those tests uses) doesn't negotiate
the modern era at all ('auto' silently falls back to legacy; {pin:
'2026-07-28'} fails outright with EraNegotiationFailed) — era negotiation
is serveStdio's own hosting behavior, not something a bare McpServer
instance implements. Real dual-era coverage only works through a spawned
process (e2e.test.ts's StdioClientTransport setup), which is too slow to
duplicate per-tool. Instead, extend e2e.test.ts's
assertRepresentativeSlice() — one call per kind of protocol-level logic
(tool registration, a key-gated short-circuit, a schema-validation
rejection, a prompt call), not per tool — and keep every call inside it
network-free (short-circuits before any fetch, or touches no client at all)
since a live Storefront/Web API call has no place in the default npm test
gate.
Anything red here is the actual finding — stop and report it before moving to live testing.
2. Safety rules for live testing (read before calling anything)
- No account is authenticated and no tool mutates state — every
mcp__steam__*tool is a read. There is nothing to revert. The actual risks here are different from the anilist/mal siblings:- Key gating. Player-data tools (
get_player_summary,get_owned_games,get_recently_played,get_player_achievements,get_friend_list,find_friends_who_own,compare_players,get_player_bans,resolve_vanity_url,get_recommended_games, and a few more insrc/tools/webPlayer.ts) requireSTEAM_API_KEY. Confirm whether the session actually has one configured (env | grep STEAM_API_KEY, or just call one and read the error; an unfilled${VAR}/$VAR/%VAR%must count as unset, not as a key) before assuming a "set STEAM_API_KEY" error is a bug rather than the intended keyless-session message — but also confirm the message itself is accurate and actionable per AGENTS.md. Store/catalog tools (search_games,get_game,get_specials,get_featured,discover_games,get_items,get_game_news,get_global_achievements,get_current_players,get_wishlist,get_followed_games,get_prices,get_review_histogram,get_game_reviews) need no key at all — see AGENTS.md's "Keyless caveat" for exactly which Web API methods are keyless by design. - Real people's data. SteamID64-taking tools resolve to real Steam accounts. Prefer well-known/public test accounts or your own profile over guessing a random person's id; a target profile must be public for most fields to return anything meaningful — a private profile returning a trimmed/limited shape (not an error) is Steam's documented behavior, not automatically a bug. Never paste a real private individual's SteamID64, friend list contents, or owned-games list into a committed test fixture or a findings report beyond what's needed to describe the bug — use a public figure's/your own id, or a well-formed-but-fake one for pure validation-boundary tests.
- Rate limits. Both the Storefront and Web API are shared, rate-limited
upstreams (
src/lib/rateLimit.tsbacks off client-side). Batch independent lookups where possible, but don't hammer the same endpoint in a tight loop just to test caching — one confirming call is enough.
- Key gating. Player-data tools (
- Read-only tools are always safe to call freely — no special permission needed, since nothing is written or shared.
3. Live edge-case sweep
Batch independent tool calls together where your harness supports it — this
is slow one-at-a-time. Adapt ids/appids/tools to whatever's currently
registered (grep -n 'registerTool(' src/tools/*.ts), don't just replay last
run's exact calls verbatim. Split into independent workstreams if your
environment supports concurrent subagents/background tasks.
- Input validation boundaries: empty string where
.min(1)is expected, negative/zero/decimal/way-past-int32 appids, a SteamID64 with the wrong digit count or out of the valid 64-bit range (e.g. accountid 0 — this has previously leaked raw upstream HTML instead of a cleanfound:false, seeCHANGELOG.md's 0.10.2 entry), batchappids/idsat their.min()/cap boundary and one past it, an unknown/misspelled param name. Include shape-valid but impossible values, not just malformed ones, and assert the result was actually filtered rather than that the call returned at all:discover_games'released_afterpassed its^\d{4}-\d{2}-\d{2}$regex on2026-13-45, and the resultingDate.parseNaN made the date filter match everything (2026-02-31silently rolled over to another date). - SteamID64 / vanity edge cases: a syntactically valid but nonexistent
SteamID64, a vanity name that doesn't resolve, a vanity name containing
URL-unsafe characters, a private profile (fields should degrade gracefully,
not error), a profile with zero games/zero friends/zero achievements (empty
array, not a crash), a VAC-banned or community-banned account via
get_player_bans. - Appid edge cases: a delisted/removed appid, a DLC or soundtrack appid
(not a base game — does the tool's shaping assume "game" fields that don't
exist?), a free-to-play appid (price fields), an appid with a region lock
(
ccparam) that changes availability/price, a non-Englishl/languageparam that changes field content, an appid that exists on the store but has no reviews yet, an appid pastget_current_players'/get_game_achievements' documented not-found behavior. - Cross-field pairing rules: filters
discover_games/get_itemssilently no-op on when a required partner is missing,check_appidsinget_owned_gamesagainst a private profile (should this degrade to "unknown" rather than a falseowned:false? see the 0.10.1 CHANGELOG entry for a real prior instance of exactly this bug), acc/lpair that Steam silently ignores for one endpoint but not another. - The same fact from two tools must agree. A per-tool sweep passes happily
while two tools answer one question differently — nothing in a single call
looks wrong. Prices are the live instance:
search_games/get_game/get_pricescome from the Storefront (price_overview/storesearch), whileget_items/discover_games/the detailed wishlist come fromGetItems' purchase options, and the two disagreed by 10x on appid 3717330 (Yakuza Kiwami: Rp 20 000 vs Rp 120 000) through several audits. So call BOTH families on the same appid and diff the numbers — then settle the tie against what the store page and Steam's own search results actually render, not against another JSON field: the search HTML displays-40% Rp 200 000 Rp 120 000while its owndata-price-finalattribute carries the wrong2000000, so a side channel agreeing with your code proves nothing. Choose appids that hit each known trap instead of a random popular game: an upgrade-SKU title whose default package group opens with an "Upgrade" sub (3717330; 518790 theHunter: Call of the Wild, which also arrivesis_free: trueon a $19.99 game — check the free/paid flag against the purchase options, not just the amount), one whose best purchase option is a bundle with another game (2186680 Warhammer 40,000: Rogue Trader, $48.36 for two games vs its own $49.99), a multi-SKU hardware app (1675200 Steam Deck), a 100%-off title (2019300) and a genuinely free one (570). The check generalizes past prices: review %, release date and compat are each reported by more than one tool from more than one upstream (release dates: Valve stamps Pacific time, so a UTC day is a day late for evening releases). - Not-found / empty-result paths: a search returning zero results, a
batch (
get_items,get_owned_games'scheck_appids) mixing valid + invalid + duplicate ids (assert a duplicate doesn't double counts —find_friends_who_ownlisted every owner twice),find_friends_who_ownwhen one friend's own lookup errors transiently (should degrade intounavailable_friends, not sink the whole call — see the just-shipped fix ingit log), an appid queried that's actually a non-game "app" (soundtrack/tool/demo) so a game-shaped assumption downstream breaks. - Payload-size risk: anything that aggregates a variable-size collection
— a very large game library (
get_owned_games), a big friend list (get_friend_list,find_friends_who_own), a long achievement list (get_player_achievements,get_global_achievements), a large wishlist (get_wishlist),discover_games/get_itemswith a wide batch of appids. Check the actual response size/token count for the largest realistic case (e.g. a Steam account with 1000+ games, a game with 200+ achievements) and at the tool's own advertised maximum, not just that it returns something (AGENTS.md requires these responses be capped/trimmed) — a documented cap the MCP client then refuses is a total failure, not a trim:get_itemsat its stated 100 appids andget_pricesat 500 each produced ~56 KB and were rejected outright for exceeding the client's per-result token limit, so the caller got nothing at all. A parameter that RAISES a cap is the same check, and it is the one that gets skipped — the cap constant is reviewed, its override's ceiling isn't.get_owned_games'limitshipped accepting 1000 (~95 KB on a 4652-game library, nearly double the threshold that had already forced two other caps down) two releases after that lesson, with its own comment claiming the bound existed so "a typo can't ask for an unbounded response". So: for everylimit/count/maxin an input schema, call it AT its maximum against the biggest real account and measure, and treat the number as a byte budget — divide the target (~28 KB here; 34 KB was measured accepted and 56 KB rejected, so 28-34 KB is margin, not failure) by one measured row, don't pick a round one. - Documented vs. actual shape: for anything that looks surprising live,
grep the field back to its
.describe()text insrc/tools/*.tsand itsformat/*.schemas.ts— does the tool's own description/outputSchema promise what you just saw (or promise something you didn't)? - Unicode / locale / injection-shaped input: emoji-only search terms,
non-Latin scripts in
search_games, whitespace-only terms, a malformedcc(country code) orl(language) value that's shaped like a real one but isn't, SQL/HTML-injection-shaped strings in any free-text param — check text fields don't come back still HTML-escaped ("), and that the error message doesn't leak raw upstream HTML/markup (a real prior bug class in this repo, seeCHANGELOG.md0.10.2) or misattribute a transport error to "not found"/"private profile" when it's actually a 5xx/network blip. A plausible-but-wrong locale can also corrupt results with no error at all: an ISOlanguage(ru, not Steam'srussian) emptied theGetTagListtag dictionary, so everytagsfilter matched nothing — indistinguishable live from "no games matched". - Systematic input-schema fuzzing across every tool: wrong JS types, invalid enums, missing required fields, malformed nested objects, extremely long strings. Only flag a genuine problem — an unhandled exception/stack trace, a confusing validation message, or (worse) malformed input silently accepted and producing a wrong result. A clean, expected Zod validation error is correct behavior, not a finding.
- Live prompt testing: run the
prompt-checkskill against every prompt insrc/tools/prompts.ts— a static read comparing prompt text against tool names/params misses argument-handling bugs that only show up when actually rendered through the real MCP protocol (this skill also covers a SteamID64-specific inspector-CLI number-precision quirk worth knowing about here).
For anything that looks like a bug, don't stop at the symptom — grep the
source for the actual mechanism (the fetch call/regex/cap that produced it)
before calling it a finding. A live response that merely looks odd but ties
back to correct, intentional code (e.g. Steam's own documented behavior for
private profiles, or discover_games's deliberate/tested precedence of an
explicit released_after over released_within_days when both are given —
check src/__tests__/steamCatalog.test.ts before flagging a cross-field
interaction, it may already be intentional and covered) isn't a finding.
The same caution runs the other way: a finding produced by reading source
without calling any live tool (e.g. a background/sub-agent doing a
static-only pass) is a hypothesis, not a confirmed bug — and so is a cheaper
sub-agent's all-clear (one reported server.json's nonexistent tools array as
matching and missed a stale host list), so spot-check its concrete claims. Steam's actual
upstream behavior sometimes contradicts what the code's shape implies (e.g.
whether a malformed id 400s cleanly vs. leaks raw HTML is something you have
to observe, not infer). Before reporting any source-only finding, spend one
live call confirming the actual response shape it depends on.
4. Source-level code review
This step is not optional polish on top of live testing — some bug classes
are only reachable here. Live-calling the real Steam API can't reliably
make one specific sub-request in a batch fail on cue, so partial-failure
resilience (Promise.allSettled/try-catch added to stop one item's failure
from sinking a batch) and where a caught error's message actually ends up
require reading the control flow, not another live call. Concretely: a
/code-review pass over the same diff caught two bugs in this exact category
that a thorough live pass had missed — a per-friend error message leaking
raw upstream content past the sanitizer, and a "fixed" resilience path still
wrapped in an outer Promise.all that silently defeated it. Don't skip this
step or treat live-testing's silence on a code path as evidence it's fine.
Sweep every file under src/tools/, src/format/, src/clients/, and
src/lib/ (lighter pass on the last group unless something specific points
there) for:
- A tool whose field name for a concept diverges from every sibling tool
handling the same concept (e.g. one player-data tool naming its
SteamID64/vanity-name parameter differently from another). Grep every call
site of a shared concept and diff the field names — don't just check each
in isolation. Every input schema in this codebase is a
z.strictObject()(since the July 2026 zod-modernization pass), so a plausible-but-wrong name is rejected outright here with a clear "Unrecognized key" error rather than silently dropped — verify live before flagging a naming mismatch as a silent-fallback bug in THIS repo specifically. The risk this check still guards against: a non-strict sibling project. Confirmed live on anilist-mcp-server (which did not have strict schemas at the time): a search tool's user-filter parameter was named differently from every other user-scoped tool, so passing the sibling-consistent (but wrong) name silently returned the unfiltered global feed instead of erroring or filtering — keep checking naming consistency here too, since a differently-but-plausibly-named field can still be a genuinely distinct, valid optional param rather than the one you meant. - A shaper in
src/format/*.tsthat dereferences a raw Steam field unguarded instead of going through its co-located*.schemas.ts'sschema.parse()— AGENTS.md requires every summarizer build its return value that way so the shaper and itsoutputSchemacan't drift. - A raw upstream HTML/error page leaking through instead of a clean
found:false/isErrorresult — this exact bug class has recurred (0.10.2's malformed-SteamID64 fix, 0.10.1'sget_current_players/get_gamefixes). Check every call site that takes a user-controlled id/appid for the same "does a 400/404/5xx get normalized, or does raw upstream body leak through." - An inference drawn from an upstream payload that cannot actually carry it.
Not "the code mishandles a field" but "the field isn't there and the code
guessed." Confirmed:
get_pricesreportedis_free: truefor any appidappdetails?filters=price_overviewanswered without a price block — but that endpoint sends an identical"data": []for a genuinely free game AND for an unreleased paid one, so freeness was never knowable from it. The result was a confident wrong answer (unreleased paid titles reported as free) that contradictedget_items, called on the same appid in the same second. For every boolean or category a summarizer DERIVES rather than copies, ask what the raw payload looks like in the other case it could be — and check by curling the upstream for both, not by reading the shaper. Two tells that this bug class is present: a sibling tool built on a different endpoint disagrees, and the test fixture's own comment asserts the guess (here,// free game: no price_overview), so the suite agrees with the bug instead of catching it. Same family as 0.13.0'sget_featured"0.00 USD" fix — which is exactly why it needs the whole-codebase sweep §6 asks for, not a one-site patch. - A degraded fallback RETURNED from inside a cache wrapper. A
catchthat turns an upstream failure into a clean empty result is correct; putting it insidewrapStaleOnError's callback is not — nor is calling a callee there that swallows its own failure (getPurchasePrices→{}cachedget_game's uncorrected price) — because the callback returning is what makesTtlCache#dedupeset()the value. Confirmed ingetGlobalAchievements/getGameAchievements: one 403 (Steam's edge answers that under load, not only for a schema-less appid) cached "this game has no achievements" for the whole TTL — exactly how the tool's own description tells the model to read an empty list — andset()it over a good-but-stale entry, destroying the fallback the wrapper exists to serve. Check everywrapStaleOnError/wrapcall site: does its callback have acatchthat RETURNS rather than rethrows? The fix is to move the catch outside the wrapper (#steamLevelis the correct shape). Where the degrade decision needs state known only inside the callback, carry it on the thrown rejection, never a closure flag —#dedupeshares one in-flight promise, so only the first concurrent caller would ever run the callback that sets it. This class is invisible to live testing: you cannot make the upstream 403 on cue, and one call in isolation returns the right answer — it is the SECOND call that is wrong. - A price read that never establishes it prices this app alone. Steam
offers several answers to "the price" and none of them means that:
appdetails/storesearchfillprice_overviewfrom the FIRST sub of the default package group (an upgrade SKU sitting there wins), andGetItems'best_purchase_optionis whatever Valve deems best value — routinely a bundle containing this app and another game — and every package of a free-to-play app is a paid add-on (CS2's Prime). Every read must go throughisOwnPackage()/ownPurchaseOption()informat/storeCard.ts(apackageid, never abundleid), so grep any newbest_purchase_option/price_overviewdereference for one that skips them — including inside filters, where it's silent:discover_games'min_discountmeasured against a bundle's discount surfaces full-price games as deals. - A shape-only validator (a regex, a bare
z.string()) whose value then feeds a computation that degrades silently instead of erroring —Date.parse→NaNmakes every</>comparison false (filter matches everything), an empty lookup table makes every membership test false (matches nothing), and both ship a confidently wrong answer as a normal-looking result. Validate the parsed value, and treat empty-but-non-null as failure: an upstream that signals errors as 200 + an empty body (GetTagListon an unrecognizedlanguage) slips past everycatch- and null-based guard. - A tool that assumes a private profile 404s instead of returning a restricted-but-200 shape (or vice versa) — Steam's actual behavior differs by endpoint, don't assume one implies the other for a sibling endpoint.
- A raw-response TS interface or a private/empty-detection helper (e.g.
isPrivate()) reused across two upstream endpoints that merely look alike (same steamid-in/games-list-out shape) — confirmed recurring:getRecentlyPlayedreusedGetOwnedGames'OwnedGamesResponse/isPrivate(keyed ongame_count), butGetRecentlyPlayedGamesactually answers with its owntotal_countfield, so a public profile with zero recent playtime ({total_count:0}, nogameskey) was misreported as private for every release through v0.10.5. Grep every reused response type/helper acrosssrc/clients/web.tsand diff the real field name each endpoint returns — don't assume a sibling call shares the same upstream field names just because the TS type does. - Missing bounds/caps on a collection-shaped response (owned games,
achievements, friend list, wishlist) that AGENTS.md says must be trimmed —
check the actual cap constant still matches what's documented in the tool's
.describe()text. - A key-gated tool in
src/tools/webPlayer.tsthat doesn't short-circuit cleanly through the shared key-check helper whenSTEAM_API_KEYis unset, or a keyless-capable tool insrc/tools/webStore.ts/storefront.tsthat accidentally requires the key when it shouldn't (re-check against AGENTS.md's "Keyless caveat" list). Also check the error-message side:HttpClient'shasCredentialsis fixed per client instance (lib/http.ts), not per request, so every keyless-capable method must overridehasCredentials: falseat its own call site — otherwise a 403 there gets misattributed to "the configured credentials are likely invalid" whenever any key is set. Diff every method underweb.ts's "keyless-capable" comment block, not just one (a past pass fixed one of five and missed four), and every OTHER client sharing the same#get/HttpClientbinding:StoreServiceClient(storeService.ts) has its ownGettype that once lacked anoptsparam, keeping the bug live a whole release past theweb.tsfix. - Tool failures that don't go through
guard()/result.ts— AGENTS.md requires every tool failure return{ isError: true }, never a raw throw. .clients/files doing any response-shaping themselves instead of leaving it entirely tosrc/format/— AGENTS.md says clients are fetch+cache only.- Logger/credential leakage: the Web API key travels as a
keyquery param — confirm the logger (src/lib/logger.ts) still redacts it in whatever log line covers the newest client call, and that nothing writes to stdout (reserved for the MCP protocol channel). - A
Promise.allSettled/try-catch newly added so one item's failure doesn't sink an entire batch call (e.g.find_friends_who_own's per-friend lookup) — this bug class needs a source-level trace, not a live call (you can't make one specific sub-request fail on cue). Check two things: (1) the rejection's raw.messagemust route throughmessageFor()(or an equivalent sanitizer), never embedded asr.reason.messagedirectly — anApiError.messagecan carry up to 500 raw chars of an upstream body perlib/http.ts'stoHttpError(e.g. an HTML error page); (2) theallSettledmust not sit inside an outerPromise.allalongside another call that can reject (e.g.#playerSummaries), which silently defeats the whole fix the moment that call fails; (3) a blanket.catch(() => undefined)must not turn a 429/5xx/timeout into a definitive answer (a vanitySTEAM_IDread "check the spelling"). Grep every sibling batching/chunking function in the same file for the identical un-fixed twin.
5. Docs/metadata consistency
Run the docs-consistency-check skill.
6. Report, then fix only what's confirmed
Rank findings by severity. For each: what's wrong, concrete repro (exact tool
call + params), the file/line causing it, and the fix shape. Silence on a
category you didn't get to (rather than implying full coverage) beats a false
"all clear." Then run the self-learning skill against each confirmed
finding.
If asked to fix: implement the smallest correct change, add/extend a test in
the matching src/__tests__/*.test.ts (mirror the existing test's style in
that file — steamFixtures.ts centralizes shared fixture shapes, reuse it
rather than inlining a new one), then re-run the full
build && test && lint && format:check gate before calling it done. Re-verify
live only after the running MCP server process has been restarted (it won't
pick up source changes on its own) — build/test passing is necessary but
re-confirming actual live behavior changed is stronger evidence than trusting
the diff alone. A fix about an upstream payload's shape must be re-verified
against the real API, never only against a fixture you wrote — that fixture
encodes the same assumption as the fix: get_featured's unpriced-title fix
tested original_price === undefined and went green, but Steam sends an
explicit null, so live output still read 0.00 USD. Correct the raw
response interface from the observed payload too (it declared number |
undefined, a shape Steam never sends).
When the bug is "we read the wrong upstream field", measure how often the
wrong one is actually wrong before switching sources — and check what the
replacement gets wrong. Generalizing from the one example that exposed the bug
produces a worse bug: over Steam's top 500 US sellers (421 priced), the
Storefront price is wrong for 2 apps while GetItems' "best" option is a
multi-game bundle for 5, so the obvious "use the modern API" fix broke more
prices than it fixed. Sample a few hundred ids, diff the two sources, and read
the disagreements one by one — they are usually several distinct causes wearing
one symptom, needing a rule (here: prefer an option naming a packageid)
rather than a swap.
Measure the cost you cite to justify a design, in the unit that matters. A narrow follow-up request was justified here on payload size (~2.2 KB → ~10.2 KB per item) without anyone timing it; measured interleaved, asking up front cost ~+40 ms on a 50-item batch — inside run-to-run noise — against ~+300 ms for the extra round-trip, so the "optimization" was a pessimization plus an extra failure path. Interleave the A/B runs (5 each, compare medians AND minimums): sequential blocks drift enough on this API to invent a difference that isn't there, and a 300-item page showed no difference at all.
Fixing one site doesn't close the class — sweep every sibling for the same
pattern. A confirmed finding is a description of a mistake the codebase is
prone to, so grep for it everywhere before calling the fix done, and re-check
the tools you did not touch. get_items' and get_prices' caps were halved
for producing responses MCP clients reject, and discover_games shipped in the
same release still uncapped at ~104 KB — its own description even tells callers
to raise count. Then re-run the checklist item that class belongs to (here,
§3's payload-size bullet) against the whole tool list, line by line: a sweep
built from your own diff only covers what you already knew was broken.
Diff every description against the behavior it describes, as its own pass.
When the same commit writes both the code and its .describe(), a mismatch
reads as correct to its author — get_recently_played shipped claiming
most-recently-played ordering while sorting by two-week playtime, which is also
what its new cap uses to decide what to discard.
7. Commit + changelog, if asked
One fix:/feat: commit per logically distinct change (don't bundle two
unrelated fixes into one commit), then a separate docs: commit adding to
CHANGELOG.md's [Unreleased] section (style: the changelog-style skill)
with one bullet per fix, each linking that fix commit's short sha
(https://github.com/Grinv/steam-games-mcp/commit/<7-char-sha>).
Author/committer Grinv <4070730+Grinv@users.noreply.github.com>, no
Co-Authored-By trailer (AGENTS.md's commit convention). Don't push unless
explicitly asked.
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 3
- Grinv/tmdb-mcp★ 513% same
- Grinv/mal-mcp★ 221% same
- Grinv/anilist-mcp-server★ 124% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository8 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- changelog-styleSkill
- docs-consistency-checkSkill
- prompt-checkSkill
- releaseSkill
- self-learningSkill
- tool-description-checkSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

