|
| 1 | +import { ResourceTemplate, type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 2 | +import { SLOT_SPECS, SLOT_FORM_FIELD } from "./storeAssets/slots.js"; |
| 3 | +import { QUERY_TYPES } from "./tools/queryWorkshop.js"; |
| 4 | +import { REVIEW_FILTERS, REVIEW_TYPES, PURCHASE_TYPES } from "./utils/reviews.js"; |
| 5 | +import { summarize, type AppData } from "./tools/getAppDetails.js"; |
| 6 | +import { steamFetch, steamStoreUrl } from "./utils/steam-api.js"; |
| 7 | + |
| 8 | +// Read-only reference data an agent (or user) can attach instead of guessing. |
| 9 | + |
| 10 | +/** Common EResult values (steamclientpublic.h) seen from the Web API. */ |
| 11 | +export const ERESULTS: Record<number, string> = { |
| 12 | + 1: "OK", |
| 13 | + 2: "Fail: generic failure", |
| 14 | + 3: "NoConnection", |
| 15 | + 8: "InvalidParam: a parameter is wrong (id, name, or format)", |
| 16 | + 9: "FileNotFound: the item, leaderboard, or file does not exist", |
| 17 | + 10: "Busy: try again later", |
| 18 | + 11: "InvalidState: the object is not in a state that allows this", |
| 19 | + 15: "AccessDenied: the key or account lacks permission, or the data is private", |
| 20 | + 16: "Timeout", |
| 21 | + 17: "Banned", |
| 22 | + 18: "AccountNotFound", |
| 23 | + 19: "InvalidSteamID", |
| 24 | + 20: "ServiceUnavailable: try again later", |
| 25 | + 21: "NotLoggedOn", |
| 26 | + 22: "Pending: the request is still processing", |
| 27 | + 24: "InsufficientPrivilege", |
| 28 | + 25: "LimitExceeded", |
| 29 | + 26: "Revoked", |
| 30 | + 27: "Expired", |
| 31 | + 29: "DuplicateRequest: this requestid already ran", |
| 32 | + 30: "AlreadyOwned", |
| 33 | + 40: "Blocked", |
| 34 | + 42: "NoMatch: nothing matched (for example an unknown vanity name)", |
| 35 | + 44: "ServiceReadOnly", |
| 36 | + 52: "Cancelled", |
| 37 | + 55: "RemoteCallFailed", |
| 38 | + 84: "RateLimitExceeded: slow down", |
| 39 | +}; |
| 40 | + |
| 41 | +function json(uri: URL, data: unknown) { |
| 42 | + return { |
| 43 | + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }], |
| 44 | + }; |
| 45 | +} |
| 46 | + |
| 47 | +export function registerResources(server: McpServer): void { |
| 48 | + server.registerResource( |
| 49 | + "store-asset-specs", |
| 50 | + "steam://specs/store-assets", |
| 51 | + { |
| 52 | + title: "Steam store asset specs", |
| 53 | + description: "Pixel sizes, formats, and alpha rules for every slot steam_validateStoreAsset checks.", |
| 54 | + mimeType: "application/json", |
| 55 | + }, |
| 56 | + (uri) => |
| 57 | + json( |
| 58 | + uri, |
| 59 | + Object.fromEntries( |
| 60 | + Object.entries(SLOT_SPECS).map(([slot, spec]) => [ |
| 61 | + slot, |
| 62 | + { ...spec, partnerUploadable: slot in SLOT_FORM_FIELD }, |
| 63 | + ]), |
| 64 | + ), |
| 65 | + ), |
| 66 | + ); |
| 67 | + |
| 68 | + server.registerResource( |
| 69 | + "eresult", |
| 70 | + "steam://enums/eresult", |
| 71 | + { |
| 72 | + title: "Steam EResult codes", |
| 73 | + description: "What common EResult numbers in Steam errors and X-eresult headers mean.", |
| 74 | + mimeType: "application/json", |
| 75 | + }, |
| 76 | + (uri) => json(uri, ERESULTS), |
| 77 | + ); |
| 78 | + |
| 79 | + server.registerResource( |
| 80 | + "ugc-query-type", |
| 81 | + "steam://enums/ugc-query-type", |
| 82 | + { |
| 83 | + title: "Workshop sort orders", |
| 84 | + description: "steam_queryWorkshop sort names and the EPublishedFileQueryType values they map to.", |
| 85 | + mimeType: "application/json", |
| 86 | + }, |
| 87 | + (uri) => json(uri, QUERY_TYPES), |
| 88 | + ); |
| 89 | + |
| 90 | + server.registerResource( |
| 91 | + "review-filters", |
| 92 | + "steam://enums/review-filters", |
| 93 | + { |
| 94 | + title: "Review filter values", |
| 95 | + description: "steam_getReviews filter, review_type, and purchase_type names and their GetAppReviews enum values.", |
| 96 | + mimeType: "application/json", |
| 97 | + }, |
| 98 | + (uri) => json(uri, { filter: REVIEW_FILTERS, review_type: REVIEW_TYPES, purchase_type: PURCHASE_TYPES }), |
| 99 | + ); |
| 100 | + |
| 101 | + server.registerResource( |
| 102 | + "app-summary", |
| 103 | + new ResourceTemplate("steam://app/{appid}/summary", { list: undefined }), |
| 104 | + { |
| 105 | + title: "Steam app summary", |
| 106 | + description: "Compact store summary for an app (the same as steam_getAppDetails' default output).", |
| 107 | + mimeType: "application/json", |
| 108 | + }, |
| 109 | + async (uri, { appid }) => { |
| 110 | + const id = Number(appid); |
| 111 | + if (!Number.isInteger(id) || id <= 0) { |
| 112 | + throw new Error(`Invalid appid in ${uri.href}`); |
| 113 | + } |
| 114 | + const data = (await steamFetch(steamStoreUrl("/api/appdetails", { appids: id }))) as Record< |
| 115 | + string, |
| 116 | + { success: boolean; data?: AppData } |
| 117 | + >; |
| 118 | + const entry = data[String(id)]; |
| 119 | + if (!entry?.success || !entry.data) { |
| 120 | + throw new Error(`App ${id} is not available in the Steam store.`); |
| 121 | + } |
| 122 | + return json(uri, summarize(entry.data)); |
| 123 | + }, |
| 124 | + ); |
| 125 | +} |
0 commit comments