pokemon-tcg-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 +46 -0
- package/dist/api.d.ts +73 -0
- package/dist/api.js +104 -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 +98 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# pokemon tcg mcp
|
|
2
|
+
|
|
3
|
+
The Pokémon TCG via [TCGdex](https://www.tcgdex.dev/), open database, no key.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
- `search_cards`, search by name, filter by rarity
|
|
8
|
+
- `get_card`, full card: HP, types, abilities, attacks, weakness, market prices
|
|
9
|
+
- `list_sets`, all sets, newest first
|
|
10
|
+
- `get_set`, a set with its full card list
|
|
11
|
+
|
|
12
|
+
## Run
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install && npm run build && node dist/index.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
> "Show me Charizard's Vivid Voltage card"
|
|
21
|
+
> `search_cards("charizard")` → `get_card("swsh4-25")`
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
node dist/index.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
32
|
+
|
|
33
|
+
## Tools at a glance
|
|
34
|
+
|
|
35
|
+
- `search_cards`: Search Pokémon TCG cards by name, optionally filtered by rarity.
|
|
36
|
+
- `get_card`: Get a single card
|
|
37
|
+
- `list_sets`: List Pokémon TCG sets (newest first).
|
|
38
|
+
- `get_set`: Get a Pokémon TCG set and its card list.
|
|
39
|
+
|
|
40
|
+
## Limits and privacy
|
|
41
|
+
|
|
42
|
+
This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required unless the project explicitly says otherwise.
|
|
43
|
+
|
|
44
|
+
## Try it
|
|
45
|
+
|
|
46
|
+
After building, connect the server through your MCP client. The repository root also contains `smoke-test.mjs` for projects covered by the shared harness. A typical tool call starts with `search_cards`.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TCGdex API v2 client — the open Pokémon TCG database.
|
|
3
|
+
* Docs: https://www.tcgdex.dev/ (free, no key)
|
|
4
|
+
*/
|
|
5
|
+
export declare class TcgError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export interface CardSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
localId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
category?: string;
|
|
13
|
+
rarity?: string;
|
|
14
|
+
set?: {
|
|
15
|
+
id?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface CardDetail {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
category?: string;
|
|
23
|
+
hp?: string;
|
|
24
|
+
types?: string[];
|
|
25
|
+
rarity?: string;
|
|
26
|
+
illustrator?: string;
|
|
27
|
+
abilities?: {
|
|
28
|
+
name?: string;
|
|
29
|
+
effect?: string;
|
|
30
|
+
}[];
|
|
31
|
+
attacks?: {
|
|
32
|
+
name?: string;
|
|
33
|
+
cost?: string[];
|
|
34
|
+
damage?: string;
|
|
35
|
+
effect?: string;
|
|
36
|
+
}[];
|
|
37
|
+
weakness?: {
|
|
38
|
+
type?: string;
|
|
39
|
+
value?: string;
|
|
40
|
+
}[];
|
|
41
|
+
resistances?: {
|
|
42
|
+
type?: string;
|
|
43
|
+
value?: string;
|
|
44
|
+
}[];
|
|
45
|
+
retreat?: number;
|
|
46
|
+
image?: string;
|
|
47
|
+
set?: {
|
|
48
|
+
id?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
};
|
|
51
|
+
variants?: {
|
|
52
|
+
firstEdition?: boolean;
|
|
53
|
+
holo?: boolean;
|
|
54
|
+
reverse?: boolean;
|
|
55
|
+
};
|
|
56
|
+
prices?: Record<string, Record<string, number>>;
|
|
57
|
+
}
|
|
58
|
+
export interface SetSummary {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
releaseDate?: string;
|
|
62
|
+
cardCount?: number;
|
|
63
|
+
logo?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function searchCards(name: string, rarity?: string, limit?: number): Promise<CardSummary[]>;
|
|
66
|
+
export declare function getCard(id: string): Promise<CardDetail | null>;
|
|
67
|
+
export declare function listSets(): Promise<SetSummary[]>;
|
|
68
|
+
export declare function getSet(id: string): Promise<SetSummary & {
|
|
69
|
+
cards: CardSummary[];
|
|
70
|
+
} | null>;
|
|
71
|
+
export declare function formatCardSummary(c: CardSummary): string;
|
|
72
|
+
export declare function formatCardDetail(c: CardDetail): string;
|
|
73
|
+
export declare function formatSet(s: SetSummary): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TCGdex API v2 client — the open Pokémon TCG database.
|
|
3
|
+
* Docs: https://www.tcgdex.dev/ (free, no key)
|
|
4
|
+
*/
|
|
5
|
+
const BASE = "https://api.tcgdex.net/v2/en";
|
|
6
|
+
export class TcgError extends Error {
|
|
7
|
+
}
|
|
8
|
+
async function getJson(path) {
|
|
9
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
10
|
+
headers: { Accept: "application/json", "User-Agent": "pokemon-tcg-mcp/1.0" },
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new TcgError(`TCGdex API error ${res.status}: ${res.statusText}`);
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Endpoints
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export async function searchCards(name, rarity, limit = 12) {
|
|
20
|
+
const rarityParam = rarity ? `&rarity=${encodeURIComponent(rarity)}` : "";
|
|
21
|
+
const data = await getJson(`/cards?name=${encodeURIComponent(name)}${rarityParam}`);
|
|
22
|
+
return (data ?? []).slice(0, limit);
|
|
23
|
+
}
|
|
24
|
+
export async function getCard(id) {
|
|
25
|
+
const data = await getJson(`/cards/${id}`);
|
|
26
|
+
return data && data.id ? data : null;
|
|
27
|
+
}
|
|
28
|
+
export async function listSets() {
|
|
29
|
+
const data = await getJson("/sets");
|
|
30
|
+
return (data ?? []).sort((a, b) => (b.releaseDate ?? "").localeCompare(a.releaseDate ?? ""));
|
|
31
|
+
}
|
|
32
|
+
export async function getSet(id) {
|
|
33
|
+
const data = await getJson(`/sets/${id}`);
|
|
34
|
+
if (!data || !data.id)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
id: data.id,
|
|
38
|
+
name: data.name ?? "?",
|
|
39
|
+
releaseDate: data.releaseDate,
|
|
40
|
+
cardCount: data.cardCount?.total ?? data.cards?.length,
|
|
41
|
+
logo: data.logo,
|
|
42
|
+
cards: (data.cards ?? []).map((c) => ({
|
|
43
|
+
id: c.id,
|
|
44
|
+
localId: c.localId,
|
|
45
|
+
name: c.name,
|
|
46
|
+
category: c.category,
|
|
47
|
+
rarity: c.rarity,
|
|
48
|
+
image: c.image,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Formatting
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export function formatCardSummary(c) {
|
|
56
|
+
return (`[${c.id}] ${c.name}` +
|
|
57
|
+
(c.rarity ? ` (${c.rarity})` : "") +
|
|
58
|
+
(c.category ? ` — ${c.category}` : "") +
|
|
59
|
+
(c.set?.name ? `\n Set: ${c.set.name}` : "") +
|
|
60
|
+
(c.image ? `\n ${c.image}` : ""));
|
|
61
|
+
}
|
|
62
|
+
export function formatCardDetail(c) {
|
|
63
|
+
const lines = [
|
|
64
|
+
`[${c.id}] ${c.name}${c.rarity ? ` (${c.rarity})` : ""}`,
|
|
65
|
+
`${c.category ?? "?"}${c.hp ? ` | HP ${c.hp}` : ""}` +
|
|
66
|
+
(c.types?.length ? ` | Types: ${c.types.join("/")}` : "") +
|
|
67
|
+
(c.retreat ? ` | Retreat: ${c.retreat}` : ""),
|
|
68
|
+
];
|
|
69
|
+
for (const a of c.abilities ?? []) {
|
|
70
|
+
lines.push(`\nAbility: ${a.name}\n${a.effect ?? ""}`);
|
|
71
|
+
}
|
|
72
|
+
for (const atk of c.attacks ?? []) {
|
|
73
|
+
lines.push(`\nAttack: ${atk.name}${atk.damage ? ` (${atk.damage})` : ""}` +
|
|
74
|
+
(atk.cost?.length ? ` [${atk.cost.join(" ")}]` : "") +
|
|
75
|
+
(atk.effect ? `\n${atk.effect}` : ""));
|
|
76
|
+
}
|
|
77
|
+
if (c.weakness?.length) {
|
|
78
|
+
lines.push(`Weakness: ${c.weakness.map((w) => `${w.type} ${w.value ?? ""}`.trim()).join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
if (c.prices) {
|
|
81
|
+
const entries = Object.entries(c.prices)
|
|
82
|
+
.map(([variant, p]) => {
|
|
83
|
+
const low = p.low ? `low $${p.low}` : "";
|
|
84
|
+
const mid = p.average ? `avg $${p.average}` : "";
|
|
85
|
+
const high = p.high ? `high $${p.high}` : "";
|
|
86
|
+
return `${variant}: ${[low, mid, high].filter(Boolean).join(" / ")}`;
|
|
87
|
+
})
|
|
88
|
+
.join(", ");
|
|
89
|
+
if (entries)
|
|
90
|
+
lines.push(`Market prices: ${entries}`);
|
|
91
|
+
}
|
|
92
|
+
if (c.illustrator)
|
|
93
|
+
lines.push(`Illustrator: ${c.illustrator}`);
|
|
94
|
+
if (c.set?.name)
|
|
95
|
+
lines.push(`Set: ${c.set.name}`);
|
|
96
|
+
if (c.image)
|
|
97
|
+
lines.push(`\n${c.image}`);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
export function formatSet(s) {
|
|
101
|
+
return (`[${s.id}] ${s.name}` +
|
|
102
|
+
(s.releaseDate ? ` — ${s.releaseDate}` : "") +
|
|
103
|
+
(s.cardCount ? ` (${s.cardCount} cards)` : ""));
|
|
104
|
+
}
|
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("Pokemon TCG 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,98 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { TcgError, formatCardDetail, formatCardSummary, formatSet, getCard, getSet, listSets, searchCards, } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "pokemon-tcg-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("search_cards", {
|
|
13
|
+
title: "Search cards",
|
|
14
|
+
description: "Search Pokémon TCG cards by name, optionally filtered by rarity.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
name: z.string().describe("Card name, e.g. 'Charizard' or 'Pikachu'"),
|
|
17
|
+
rarity: z.string().optional().describe("e.g. 'Rare Holo', 'Common', 'Rare Holo EX'"),
|
|
18
|
+
limit: z.number().int().min(1).max(25).default(12).describe("Max results"),
|
|
19
|
+
}),
|
|
20
|
+
annotations: READ_ONLY,
|
|
21
|
+
}, async ({ name, rarity, limit }) => {
|
|
22
|
+
try {
|
|
23
|
+
const cards = await searchCards(name, rarity, limit);
|
|
24
|
+
if (cards.length === 0) {
|
|
25
|
+
return text(`No cards found for "${name}"${rarity ? ` (${rarity})` : ""}.`);
|
|
26
|
+
}
|
|
27
|
+
return text(`Cards matching "${name}":\n` +
|
|
28
|
+
cards.map((c, i) => `${i + 1}. ${formatCardSummary(c)}`).join("\n\n"));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return textError(errorMessage(e));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
server.registerTool("get_card", {
|
|
35
|
+
title: "Get card",
|
|
36
|
+
description: "Get a single card's full detail: attacks, abilities, weakness, " +
|
|
37
|
+
"variants, and market prices when available.",
|
|
38
|
+
inputSchema: z.object({ id: z.string().describe("Card id from search_cards, e.g. 'swsh4-25'") }),
|
|
39
|
+
annotations: READ_ONLY,
|
|
40
|
+
}, async ({ id }) => {
|
|
41
|
+
try {
|
|
42
|
+
const card = await getCard(id);
|
|
43
|
+
if (!card)
|
|
44
|
+
return text(`No card with id "${id}".`);
|
|
45
|
+
return text(formatCardDetail(card));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return textError(errorMessage(e));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.registerTool("list_sets", {
|
|
52
|
+
title: "List sets",
|
|
53
|
+
description: "List Pokémon TCG sets (newest first).",
|
|
54
|
+
inputSchema: z.object({}),
|
|
55
|
+
annotations: READ_ONLY,
|
|
56
|
+
}, async () => {
|
|
57
|
+
try {
|
|
58
|
+
const sets = await listSets();
|
|
59
|
+
if (sets.length === 0)
|
|
60
|
+
return text("No sets found.");
|
|
61
|
+
return text(`Pokémon TCG sets (${sets.length}):\n` +
|
|
62
|
+
sets.slice(0, 20).map((s, i) => `${i + 1}. ${formatSet(s)}`).join("\n"));
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return textError(errorMessage(e));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
server.registerTool("get_set", {
|
|
69
|
+
title: "Get set",
|
|
70
|
+
description: "Get a Pokémon TCG set and its card list.",
|
|
71
|
+
inputSchema: z.object({ id: z.string().describe("Set id from list_sets, e.g. 'swsh4' or 'base1'") }),
|
|
72
|
+
annotations: READ_ONLY,
|
|
73
|
+
}, async ({ id }) => {
|
|
74
|
+
try {
|
|
75
|
+
const set = await getSet(id);
|
|
76
|
+
if (!set)
|
|
77
|
+
return text(`No set with id "${id}".`);
|
|
78
|
+
const head = `${formatSet(set)} (${set.cards.length} cards)\n`;
|
|
79
|
+
const body = set.cards
|
|
80
|
+
.slice(0, 25)
|
|
81
|
+
.map((c) => `• #${c.localId} ${c.name}${c.rarity ? ` (${c.rarity})` : ""}`)
|
|
82
|
+
.join("\n");
|
|
83
|
+
const tail = set.cards.length > 25 ? `\n…and ${set.cards.length - 25} more` : "";
|
|
84
|
+
return text(head + body + tail);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return textError(errorMessage(e));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return server;
|
|
91
|
+
}
|
|
92
|
+
function errorMessage(e) {
|
|
93
|
+
if (e instanceof TcgError)
|
|
94
|
+
return `Error: ${e.message}`;
|
|
95
|
+
if (e instanceof Error)
|
|
96
|
+
return `Error: ${e.message}`;
|
|
97
|
+
return `Error: ${String(e)}`;
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pokemon-tcg-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Use this MCP server to the Pok\u00e9mon TCG, search cards, sets, rarities via TCGdex. Tools include search cards, get card, list sets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/pokemon-tcg-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/pokemon-tcg-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"pokemon-tcg-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
|
+
"pokemon",
|
|
27
|
+
"tcg",
|
|
28
|
+
"cards",
|
|
29
|
+
"trading-cards",
|
|
30
|
+
"tcgdex"
|
|
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
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
}
|
|
44
|
+
}
|