@mrfentmen/steam-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/api.d.ts +53 -0
- package/dist/api.js +120 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +63 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# steam-mcp
|
|
2
|
+
|
|
3
|
+
Steam store search, prices and discounts, app details, and game news.
|
|
4
|
+
Uses Steam's keyless store + news endpoints.
|
|
5
|
+
|
|
6
|
+
## Tools
|
|
7
|
+
|
|
8
|
+
- `search_games` — store search with current prices / discount percentages
|
|
9
|
+
- `get_game_details` — price, release date, developers, genres, Metacritic, blurb
|
|
10
|
+
- `get_game_news` — recent Steam news posts for a game
|
|
11
|
+
|
|
12
|
+
## Run
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install && npm run build && node dist/index.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
> "What's Helldivers 2 cost and is it on sale?"
|
|
21
|
+
> `search_games("helldivers")` → `get_game_details(553850)`
|
|
22
|
+
|
|
23
|
+
> "What's new for Cyberpunk?"
|
|
24
|
+
> `get_game_news(1091500)`
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steam client — keyless store endpoints.
|
|
3
|
+
* - Store search: /api/storesearch
|
|
4
|
+
* - App details: /api/appdetails (filtered to keep payloads small)
|
|
5
|
+
* - News: ISteamNews/GetNewsForApp (Steamworks Web API, no key
|
|
6
|
+
* required for this endpoint)
|
|
7
|
+
*/
|
|
8
|
+
export declare class SteamError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export interface StoreItem {
|
|
11
|
+
type: string;
|
|
12
|
+
appid: number;
|
|
13
|
+
name: string;
|
|
14
|
+
price?: {
|
|
15
|
+
currency: string;
|
|
16
|
+
initial: number;
|
|
17
|
+
final: number;
|
|
18
|
+
discount_percent: number;
|
|
19
|
+
};
|
|
20
|
+
tinyImage?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AppDetail {
|
|
23
|
+
appid: number;
|
|
24
|
+
name: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
isFree?: boolean;
|
|
27
|
+
price: {
|
|
28
|
+
currency: string;
|
|
29
|
+
initial: number;
|
|
30
|
+
final: number;
|
|
31
|
+
discountPercent: number;
|
|
32
|
+
} | null;
|
|
33
|
+
releaseDate?: string;
|
|
34
|
+
developers: string[];
|
|
35
|
+
genres: string[];
|
|
36
|
+
metacritic?: number;
|
|
37
|
+
description?: string;
|
|
38
|
+
headerImage?: string;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
41
|
+
export interface NewsItem {
|
|
42
|
+
title: string;
|
|
43
|
+
url: string;
|
|
44
|
+
author?: string;
|
|
45
|
+
date?: string;
|
|
46
|
+
contents?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function searchStore(query: string): Promise<StoreItem[]>;
|
|
49
|
+
export declare function getAppDetail(appid: number): Promise<AppDetail | null>;
|
|
50
|
+
export declare function getNews(appid: number, count: number): Promise<NewsItem[]>;
|
|
51
|
+
export declare function fmtPrice(cents: number, currency?: string): string;
|
|
52
|
+
export declare function formatStoreItem(i: StoreItem): string;
|
|
53
|
+
export declare function formatAppDetail(a: AppDetail): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steam client — keyless store endpoints.
|
|
3
|
+
* - Store search: /api/storesearch
|
|
4
|
+
* - App details: /api/appdetails (filtered to keep payloads small)
|
|
5
|
+
* - News: ISteamNews/GetNewsForApp (Steamworks Web API, no key
|
|
6
|
+
* required for this endpoint)
|
|
7
|
+
*/
|
|
8
|
+
const STORE = "https://store.steampowered.com/api";
|
|
9
|
+
const NEWS = "https://api.steampowered.com/ISteamNews/GetNewsForApp/v2";
|
|
10
|
+
export class SteamError extends Error {
|
|
11
|
+
}
|
|
12
|
+
async function getJson(url) {
|
|
13
|
+
const res = await fetch(url, {
|
|
14
|
+
headers: { Accept: "application/json", "User-Agent": "steam-mcp/1.0" },
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok)
|
|
17
|
+
throw new SteamError(`Steam API error ${res.status}: ${res.statusText}`);
|
|
18
|
+
return (await res.json());
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Endpoints
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export async function searchStore(query) {
|
|
24
|
+
const data = await getJson(`${STORE}/storesearch/?term=${encodeURIComponent(query)}&cc=US&l=en`);
|
|
25
|
+
return (data.items ?? [])
|
|
26
|
+
.filter((i) => i.type === "app")
|
|
27
|
+
.slice(0, 12)
|
|
28
|
+
.map((i) => ({ ...i, appid: i.id }));
|
|
29
|
+
}
|
|
30
|
+
export async function getAppDetail(appid) {
|
|
31
|
+
const filters = [
|
|
32
|
+
"basic",
|
|
33
|
+
"price_overview",
|
|
34
|
+
"release_date",
|
|
35
|
+
"developers",
|
|
36
|
+
"genres",
|
|
37
|
+
"metacritic",
|
|
38
|
+
"short_description",
|
|
39
|
+
"header_image",
|
|
40
|
+
].join(",");
|
|
41
|
+
const data = await getJson(`${STORE}/appdetails?appids=${appid}&cc=US&l=en&filters=${filters}`);
|
|
42
|
+
const entry = data[String(appid)];
|
|
43
|
+
if (!entry?.success || !entry.data)
|
|
44
|
+
return null;
|
|
45
|
+
const d = entry.data;
|
|
46
|
+
const price = d.price_overview;
|
|
47
|
+
return {
|
|
48
|
+
appid,
|
|
49
|
+
name: d.name ?? "?",
|
|
50
|
+
type: d.type,
|
|
51
|
+
isFree: d.is_free,
|
|
52
|
+
price: price
|
|
53
|
+
? {
|
|
54
|
+
currency: price.currency ?? "USD",
|
|
55
|
+
initial: price.initial ?? 0,
|
|
56
|
+
final: price.final ?? 0,
|
|
57
|
+
discountPercent: price.discount_percent ?? 0,
|
|
58
|
+
}
|
|
59
|
+
: null,
|
|
60
|
+
releaseDate: d.release_date?.date,
|
|
61
|
+
developers: d.developers ?? [],
|
|
62
|
+
genres: (d.genres ?? []).map((g) => g.description),
|
|
63
|
+
metacritic: d.metacritic?.score,
|
|
64
|
+
description: d.short_description,
|
|
65
|
+
headerImage: d.header_image,
|
|
66
|
+
url: `https://store.steampowered.com/app/${appid}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function getNews(appid, count) {
|
|
70
|
+
const data = await getJson(`${NEWS}/?appid=${appid}&count=${count}&maxlength=600`);
|
|
71
|
+
return (data.appnews?.newsitems ?? []).map((n) => ({
|
|
72
|
+
title: n.title ?? "?",
|
|
73
|
+
url: n.url ?? "",
|
|
74
|
+
author: n.author,
|
|
75
|
+
date: n.date ? new Date(n.date * 1000).toISOString().slice(0, 10) : undefined,
|
|
76
|
+
contents: n.contents ? stripHtml(n.contents).slice(0, 300) : undefined,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Formatting
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
export function fmtPrice(cents, currency = "USD") {
|
|
83
|
+
const symbol = currency === "USD" ? "$" : `${currency} `;
|
|
84
|
+
return `${symbol}${(cents / 100).toFixed(2)}`;
|
|
85
|
+
}
|
|
86
|
+
export function formatStoreItem(i) {
|
|
87
|
+
const price = i.price
|
|
88
|
+
? i.price.discount_percent > 0
|
|
89
|
+
? `~~${fmtPrice(i.price.initial, i.price.currency)}~~ → **${fmtPrice(i.price.final, i.price.currency)}** (-${i.price.discount_percent}%)`
|
|
90
|
+
: fmtPrice(i.price.final, i.price.currency)
|
|
91
|
+
: "no price data";
|
|
92
|
+
return `[${i.appid}] ${i.name}\n ${price}\n https://store.steampowered.com/app/${i.appid}`;
|
|
93
|
+
}
|
|
94
|
+
export function formatAppDetail(a) {
|
|
95
|
+
const lines = [
|
|
96
|
+
`[${a.appid}] ${a.name}${a.type ? ` (${a.type})` : ""}`,
|
|
97
|
+
a.price
|
|
98
|
+
? a.price.discountPercent > 0
|
|
99
|
+
? `Price: ~~${fmtPrice(a.price.initial)}~~ → ${fmtPrice(a.price.final)} (-${a.price.discountPercent}%)`
|
|
100
|
+
: `Price: ${fmtPrice(a.price.final)}`
|
|
101
|
+
: a.isFree
|
|
102
|
+
? "Free to play"
|
|
103
|
+
: "Price unavailable",
|
|
104
|
+
a.releaseDate ? `Released: ${a.releaseDate}` : "",
|
|
105
|
+
a.developers.length ? `Developers: ${a.developers.join(", ")}` : "",
|
|
106
|
+
a.genres.length ? `Genres: ${a.genres.join(", ")}` : "",
|
|
107
|
+
a.metacritic ? `Metacritic: ${a.metacritic}` : "",
|
|
108
|
+
].filter(Boolean);
|
|
109
|
+
if (a.description)
|
|
110
|
+
lines.push(`\n${a.description}`);
|
|
111
|
+
lines.push(`\n${a.url}`);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function stripHtml(s) {
|
|
115
|
+
return s
|
|
116
|
+
.replace(/<[^>]+>/g, " ")
|
|
117
|
+
.replace(/\[\[([^\]]*)\]\]/g, "")
|
|
118
|
+
.replace(/\s+/g, " ")
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("Steam MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SteamError, formatAppDetail, formatStoreItem, getAppDetail, getNews, searchStore, } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
export function createServer() {
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "steam-mcp",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
});
|
|
10
|
+
server.tool("search_games", "Search the Steam store by title. Returns appids with current prices " +
|
|
11
|
+
"and discounts.", { query: z.string().describe("Game title, e.g. 'Helldivers' or 'Baldur'") }, async ({ query }) => {
|
|
12
|
+
try {
|
|
13
|
+
const items = await searchStore(query);
|
|
14
|
+
if (items.length === 0)
|
|
15
|
+
return text(`Nothing on Steam matches "${query}".`);
|
|
16
|
+
return text(`Steam results for "${query}":\n` +
|
|
17
|
+
items.map((i, n) => `${n + 1}. ${formatStoreItem(i)}`).join("\n"));
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return text(errorMessage(e));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
server.tool("get_game_details", "Get details for a Steam app: price/discount, release date, " +
|
|
24
|
+
"developers, genres, Metacritic, description.", { appid: z.number().int().describe("Steam app id from search_games") }, async ({ appid }) => {
|
|
25
|
+
try {
|
|
26
|
+
const detail = await getAppDetail(appid);
|
|
27
|
+
if (!detail)
|
|
28
|
+
return text(`No Steam app with id ${appid}.`);
|
|
29
|
+
return text(formatAppDetail(detail));
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return text(errorMessage(e));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
server.tool("get_game_news", "Fetch recent Steam news posts for a game.", {
|
|
36
|
+
appid: z.number().int().describe("Steam app id"),
|
|
37
|
+
count: z.number().int().min(1).max(20).default(5).describe("Number of posts"),
|
|
38
|
+
}, async ({ appid, count }) => {
|
|
39
|
+
try {
|
|
40
|
+
const news = await getNews(appid, count);
|
|
41
|
+
if (news.length === 0)
|
|
42
|
+
return text(`No news for app ${appid}.`);
|
|
43
|
+
return text(`Latest news for app ${appid}:\n` +
|
|
44
|
+
news
|
|
45
|
+
.map((n, i) => `${i + 1}. ${n.title}${n.date ? ` (${n.date})` : ""}` +
|
|
46
|
+
(n.author ? ` by ${n.author}` : "") +
|
|
47
|
+
`\n ${n.url}` +
|
|
48
|
+
(n.contents ? `\n ${n.contents}` : ""))
|
|
49
|
+
.join("\n\n"));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return text(errorMessage(e));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return server;
|
|
56
|
+
}
|
|
57
|
+
function errorMessage(e) {
|
|
58
|
+
if (e instanceof SteamError)
|
|
59
|
+
return `Error: ${e.message}`;
|
|
60
|
+
if (e instanceof Error)
|
|
61
|
+
return `Error: ${e.message}`;
|
|
62
|
+
return `Error: ${String(e)}`;
|
|
63
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrfentmen/steam-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Steam — store search, prices and discounts, app details, and game news.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/steam-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/steam-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"steam-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"steam",
|
|
27
|
+
"games",
|
|
28
|
+
"prices",
|
|
29
|
+
"store",
|
|
30
|
+
"deals"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
35
|
+
"zod": "^3.23.8"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
}
|
|
41
|
+
}
|