unofficial-ravensburger-playhub-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.
@@ -0,0 +1,230 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { formatStore, formatEvent, formatStandingEntry, formatRegistrationEntry, } from "../lib/formatters.js";
4
+ describe("formatStore", () => {
5
+ it("formats minimal store with name and id", () => {
6
+ const gameStore = {
7
+ id: "gs1",
8
+ store: { id: 1, name: "Test Store" },
9
+ store_types: [],
10
+ store_types_pretty: [],
11
+ };
12
+ const out = formatStore(gameStore);
13
+ assert.ok(out.includes("**Test Store**"));
14
+ assert.ok(out.includes("Store ID: 1"));
15
+ });
16
+ it("includes address, phone, email, website when present", () => {
17
+ const gameStore = {
18
+ id: "gs1",
19
+ store: {
20
+ id: 1,
21
+ name: "Full Store",
22
+ full_address: "123 Main St",
23
+ phone_number: "555-1234",
24
+ email: "store@example.com",
25
+ website: "https://store.example.com",
26
+ },
27
+ store_types: [],
28
+ store_types_pretty: [],
29
+ };
30
+ const out = formatStore(gameStore);
31
+ assert.ok(out.includes("📍 123 Main St"));
32
+ assert.ok(out.includes("📞 555-1234"));
33
+ assert.ok(out.includes("📧 store@example.com"));
34
+ assert.ok(out.includes("🌐 https://store.example.com"));
35
+ });
36
+ it("includes store_types_pretty when present", () => {
37
+ const gameStore = {
38
+ id: "gs1",
39
+ store: { id: 1, name: "Store" },
40
+ store_types: ["flgs"],
41
+ store_types_pretty: ["Friendly Local Game Store"],
42
+ };
43
+ const out = formatStore(gameStore);
44
+ assert.ok(out.includes("🏷️ Types: Friendly Local Game Store"));
45
+ });
46
+ it("includes bio when present", () => {
47
+ const gameStore = {
48
+ id: "gs1",
49
+ store: { id: 1, name: "Store", bio: "We sell cards." },
50
+ store_types: [],
51
+ store_types_pretty: [],
52
+ };
53
+ const out = formatStore(gameStore);
54
+ assert.ok(out.includes("We sell cards."));
55
+ });
56
+ it("includes social links when present", () => {
57
+ const gameStore = {
58
+ id: "gs1",
59
+ store: {
60
+ id: 1,
61
+ name: "Store",
62
+ discord_url: "https://discord.gg/abc",
63
+ facebook_url: "https://facebook.com/store",
64
+ instagram_handle: "store",
65
+ twitter_handle: "store",
66
+ },
67
+ store_types: [],
68
+ store_types_pretty: [],
69
+ };
70
+ const out = formatStore(gameStore);
71
+ assert.ok(out.includes("Discord: https://discord.gg/abc"));
72
+ assert.ok(out.includes("Facebook: https://facebook.com/store"));
73
+ assert.ok(out.includes("Instagram: @store"));
74
+ assert.ok(out.includes("Twitter: @store"));
75
+ });
76
+ });
77
+ describe("formatEvent", () => {
78
+ it("formats minimal event with name and id", () => {
79
+ const event = {
80
+ id: 100,
81
+ name: "Weekly League",
82
+ start_datetime: "2025-02-01T18:00:00Z",
83
+ };
84
+ const out = formatEvent(event);
85
+ assert.ok(out.includes("**Weekly League**"));
86
+ assert.ok(out.includes("ID: 100"));
87
+ assert.ok(out.includes("📅"));
88
+ });
89
+ it("includes format, category, store, address when present", () => {
90
+ const event = {
91
+ id: 101,
92
+ name: "Event",
93
+ start_datetime: "2025-02-01T18:00:00Z",
94
+ gameplay_format: { id: "f1", name: "Constructed" },
95
+ event_configuration_template: "template-id",
96
+ store: { id: 1, name: "Game Store" },
97
+ full_address: "456 Oak Ave",
98
+ };
99
+ const out = formatEvent(event);
100
+ assert.ok(out.includes("🎮 Format: Constructed"));
101
+ assert.ok(out.includes("📁 Category:"));
102
+ assert.ok(out.includes("🏪 Store: Game Store"));
103
+ assert.ok(out.includes("📍 456 Oak Ave"));
104
+ });
105
+ it("includes distance when present", () => {
106
+ const event = {
107
+ id: 102,
108
+ name: "Event",
109
+ start_datetime: "2025-02-01T18:00:00Z",
110
+ distance_in_miles: 5.5,
111
+ };
112
+ const out = formatEvent(event);
113
+ assert.ok(out.includes("🚗 Distance: 5.5 miles"));
114
+ });
115
+ it("shows paid entry when cost_in_cents > 0", () => {
116
+ const event = {
117
+ id: 103,
118
+ name: "Event",
119
+ start_datetime: "2025-02-01T18:00:00Z",
120
+ cost_in_cents: 1500,
121
+ currency: "USD",
122
+ };
123
+ const out = formatEvent(event);
124
+ assert.ok(out.includes("💰 Entry: USD $15.00"));
125
+ });
126
+ it("shows free entry when cost_in_cents is 0", () => {
127
+ const event = {
128
+ id: 104,
129
+ name: "Event",
130
+ start_datetime: "2025-02-01T18:00:00Z",
131
+ cost_in_cents: 0,
132
+ };
133
+ const out = formatEvent(event);
134
+ assert.ok(out.includes("💰 Entry: Free"));
135
+ });
136
+ it("shows participants when capacity present", () => {
137
+ const event = {
138
+ id: 105,
139
+ name: "Event",
140
+ start_datetime: "2025-02-01T18:00:00Z",
141
+ capacity: 32,
142
+ registered_user_count: 20,
143
+ };
144
+ const out = formatEvent(event);
145
+ assert.ok(out.includes("👥 Participants: 20/32"));
146
+ });
147
+ it("shows registered count when no capacity", () => {
148
+ const event = {
149
+ id: 106,
150
+ name: "Event",
151
+ start_datetime: "2025-02-01T18:00:00Z",
152
+ registered_user_count: 8,
153
+ };
154
+ const out = formatEvent(event);
155
+ assert.ok(out.includes("👥 Registered: 8"));
156
+ });
157
+ it("includes display_status and settings when present", () => {
158
+ const event = {
159
+ id: 107,
160
+ name: "Event",
161
+ start_datetime: "2025-02-01T18:00:00Z",
162
+ display_status: "Upcoming",
163
+ settings: { event_lifecycle_status: "REGISTRATION_OPEN" },
164
+ };
165
+ const out = formatEvent(event);
166
+ assert.ok(out.includes("📊 Status: Upcoming"));
167
+ assert.ok(out.includes("🎟️ Registration: registration open"));
168
+ });
169
+ it("includes featured and online flags when true", () => {
170
+ const event = {
171
+ id: 108,
172
+ name: "Event",
173
+ start_datetime: "2025-02-01T18:00:00Z",
174
+ is_headlining_event: true,
175
+ event_is_online: true,
176
+ };
177
+ const out = formatEvent(event);
178
+ assert.ok(out.includes("⭐ Featured Event"));
179
+ assert.ok(out.includes("🌐 Online Event"));
180
+ });
181
+ it("includes description when present", () => {
182
+ const event = {
183
+ id: 109,
184
+ name: "Event",
185
+ start_datetime: "2025-02-01T18:00:00Z",
186
+ description: "Bring your deck!",
187
+ };
188
+ const out = formatEvent(event);
189
+ assert.ok(out.includes("Bring your deck!"));
190
+ });
191
+ });
192
+ describe("formatStandingEntry", () => {
193
+ it("falls back to username when player_name and display_name missing", () => {
194
+ const entry = { username: "player1" };
195
+ const out = formatStandingEntry(entry, 0);
196
+ assert.ok(out.includes("1. player1"));
197
+ });
198
+ it("uses em dash when no name fields", () => {
199
+ const entry = {};
200
+ const out = formatStandingEntry(entry, 0);
201
+ assert.ok(out.includes("1. —"));
202
+ });
203
+ it("shows record with only wins (losses 0)", () => {
204
+ const entry = { player_name: "A", wins: 3 };
205
+ const out = formatStandingEntry(entry, 0);
206
+ assert.ok(out.includes("Record: 3-0"));
207
+ });
208
+ });
209
+ describe("formatRegistrationEntry", () => {
210
+ it("falls back to em dash when no name available", () => {
211
+ const entry = {};
212
+ const out = formatRegistrationEntry(entry, 0);
213
+ assert.ok(out.includes("1. —"));
214
+ });
215
+ it("uses user.username when display_name missing", () => {
216
+ const entry = { user: { username: "u1" } };
217
+ const out = formatRegistrationEntry(entry, 0);
218
+ assert.ok(out.includes("1. u1"));
219
+ });
220
+ it("includes Registered line for invalid date string (Invalid Date or raw fallback)", () => {
221
+ const entry = {
222
+ display_name: "X",
223
+ registered_at: "not-a-date",
224
+ };
225
+ const out = formatRegistrationEntry(entry, 0);
226
+ assert.ok(out.includes("Registered:"), "should include Registered line");
227
+ // new Date("not-a-date") does not throw; toLocaleString() yields "Invalid Date" in most envs
228
+ assert.ok(out.includes("Invalid Date") || out.includes("not-a-date"), "should show Invalid Date or raw value");
229
+ });
230
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Integration tests: spawn the Lorcana Event Finder MCP server and run each tool
3
+ * with one or more call variations to ensure the server and all tools work end-to-end.
4
+ *
5
+ * Run after build: npm run build && npm test
6
+ * Requires network (Ravensburger API, Nominatim geocoding).
7
+ */
8
+ export {};
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Integration tests: spawn the Lorcana Event Finder MCP server and run each tool
3
+ * with one or more call variations to ensure the server and all tools work end-to-end.
4
+ *
5
+ * Run after build: npm run build && npm test
6
+ * Requires network (Ravensburger API, Nominatim geocoding).
7
+ */
8
+ import { describe, it, before, after } from "node:test";
9
+ import assert from "node:assert";
10
+ import { resolve, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { Client } from "@modelcontextprotocol/sdk/client";
13
+ // StdioClientTransport is not re-exported from the client entry; load from SDK dist.
14
+ import { StdioClientTransport } from "../../node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js";
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ // When run from dist/test/, go up to repo root
17
+ const projectRoot = resolve(__dirname, "..", "..");
18
+ const EXPECTED_TOOL_NAMES = [
19
+ "list_capabilities",
20
+ "list_filters",
21
+ "search_events",
22
+ "get_event_details",
23
+ "get_tournament_round_standings",
24
+ "get_event_registrations",
25
+ "search_events_by_city",
26
+ "search_stores",
27
+ "search_stores_by_city",
28
+ ];
29
+ describe("MCP server integration – all tools and call variations", { timeout: 60_000 }, () => {
30
+ let client;
31
+ let transport;
32
+ before(async () => {
33
+ transport = new StdioClientTransport({
34
+ command: "node",
35
+ args: [resolve(projectRoot, "dist/index.js")],
36
+ cwd: projectRoot,
37
+ stderr: "pipe",
38
+ });
39
+ client = new Client({ name: "mcp-tools-integration-test", version: "1.0.0" }, { capabilities: {} });
40
+ await client.connect(transport);
41
+ });
42
+ after(async () => {
43
+ await transport.close();
44
+ });
45
+ it("lists all expected tools", async () => {
46
+ const { tools } = await client.listTools();
47
+ const names = tools.map((t) => t.name).sort();
48
+ const expected = [...EXPECTED_TOOL_NAMES].sort();
49
+ assert.deepStrictEqual(names, expected, `Expected tools ${expected.join(", ")}; got ${names.join(", ")}`);
50
+ });
51
+ async function callTool(name, args) {
52
+ const result = await client.callTool({ name, arguments: args });
53
+ assert.ok(result, "callTool should return a result");
54
+ if ("content" in result && Array.isArray(result.content)) {
55
+ const textPart = result.content.find((c) => c.type === "text" && "text" in c);
56
+ return {
57
+ content: result.content,
58
+ text: textPart && "text" in textPart ? textPart.text : "",
59
+ isError: "isError" in result ? !!result.isError : false,
60
+ };
61
+ }
62
+ return { content: result, text: "", isError: false };
63
+ }
64
+ it("list_capabilities – no args", async () => {
65
+ const { text, isError } = await callTool("list_capabilities", {});
66
+ assert.ok(!isError, `list_capabilities should not error: ${text}`);
67
+ assert.ok(text.includes("search_events"), "capabilities should mention search_events");
68
+ assert.ok(text.includes("search_events_by_city"), "capabilities should mention search_events_by_city");
69
+ assert.ok(text.includes("get_event_details") || text.includes("get_event"), "capabilities should mention event details");
70
+ assert.ok(text.includes("list_filters"), "capabilities should mention list_filters");
71
+ });
72
+ it("list_filters – no args", async () => {
73
+ const { text, isError } = await callTool("list_filters", {});
74
+ assert.ok(!isError, `list_filters should not error: ${text}`);
75
+ assert.ok(text.includes("Formats") || text.includes("formats"), "list_filters should include formats");
76
+ assert.ok(text.includes("Categories") || text.includes("categories"), "list_filters should include categories");
77
+ assert.ok(text.includes("Example") || text.includes("formats"), "list_filters should include usage example or param names");
78
+ });
79
+ it("search_events – required only", async () => {
80
+ const { text, isError } = await callTool("search_events", {
81
+ latitude: 42.33,
82
+ longitude: -83.05,
83
+ });
84
+ assert.ok(!isError, `search_events should not error: ${text}`);
85
+ assert.ok(typeof text === "string" && text.length > 0, "should return non-empty text");
86
+ });
87
+ it("search_events – with optional params", async () => {
88
+ const { text, isError } = await callTool("search_events", {
89
+ latitude: 42.33,
90
+ longitude: -83.05,
91
+ radius_miles: 10,
92
+ page: 1,
93
+ page_size: 5,
94
+ statuses: ["upcoming", "inProgress"],
95
+ featured_only: false,
96
+ });
97
+ assert.ok(!isError, `search_events (optional) should not error: ${text}`);
98
+ assert.ok(typeof text === "string", "should return text");
99
+ });
100
+ it("search_events – with text_search", async () => {
101
+ const { text, isError } = await callTool("search_events", {
102
+ latitude: 42.33,
103
+ longitude: -83.05,
104
+ radius_miles: 50,
105
+ text_search: "Lorcana",
106
+ page: 1,
107
+ page_size: 5,
108
+ });
109
+ assert.ok(!isError, `search_events (text_search) should not error: ${text}`);
110
+ assert.ok(typeof text === "string", "should return text");
111
+ });
112
+ it("search_events – with store_id", async () => {
113
+ const { text, isError } = await callTool("search_events", {
114
+ latitude: 42.33,
115
+ longitude: -83.05,
116
+ radius_miles: 100,
117
+ store_id: 1,
118
+ page: 1,
119
+ page_size: 5,
120
+ });
121
+ assert.ok(!isError, `search_events (store_id) should not error: ${text}`);
122
+ assert.ok(typeof text === "string", "should return text");
123
+ });
124
+ it("search_events – with start_date", async () => {
125
+ const { text, isError } = await callTool("search_events", {
126
+ latitude: 42.33,
127
+ longitude: -83.05,
128
+ start_date: "2025-01-01",
129
+ page: 1,
130
+ page_size: 5,
131
+ });
132
+ assert.ok(!isError, `search_events (start_date) should not error: ${text}`);
133
+ assert.ok(typeof text === "string", "should return text");
134
+ });
135
+ it("get_event_details – valid-looking id", async () => {
136
+ const { text, isError } = await callTool("get_event_details", { event_id: 1 });
137
+ // API may return 404 for id=1; server still returns content (possibly error message)
138
+ assert.ok(typeof text === "string" && text.length > 0, "should return some content");
139
+ });
140
+ it("get_tournament_round_standings – required only", async () => {
141
+ const { text, isError } = await callTool("get_tournament_round_standings", {
142
+ round_id: 414976,
143
+ });
144
+ assert.ok(!isError, `get_tournament_round_standings should not error: ${text}`);
145
+ assert.ok(typeof text === "string", "should return text");
146
+ });
147
+ it("get_tournament_round_standings – with pagination", async () => {
148
+ const { text, isError } = await callTool("get_tournament_round_standings", {
149
+ round_id: 414976,
150
+ page: 1,
151
+ page_size: 10,
152
+ });
153
+ assert.ok(!isError, `get_tournament_round_standings (pagination) should not error: ${text}`);
154
+ assert.ok(typeof text === "string", "should return text");
155
+ });
156
+ it("get_event_registrations – required only", async () => {
157
+ const { text, isError } = await callTool("get_event_registrations", {
158
+ event_id: 333450,
159
+ });
160
+ assert.ok(!isError, `get_event_registrations should not error: ${text}`);
161
+ assert.ok(typeof text === "string", "should return text");
162
+ });
163
+ it("get_event_registrations – with pagination", async () => {
164
+ const { text, isError } = await callTool("get_event_registrations", {
165
+ event_id: 333450,
166
+ page: 1,
167
+ page_size: 10,
168
+ });
169
+ assert.ok(!isError, `get_event_registrations (pagination) should not error: ${text}`);
170
+ assert.ok(typeof text === "string", "should return text");
171
+ });
172
+ it("search_events_by_city – required only", async () => {
173
+ const { text, isError } = await callTool("search_events_by_city", {
174
+ city: "Detroit, MI",
175
+ });
176
+ assert.ok(!isError, `search_events_by_city should not error: ${text}`);
177
+ assert.ok(typeof text === "string", "should return text");
178
+ });
179
+ it("search_events_by_city – with optional params", async () => {
180
+ const { text, isError } = await callTool("search_events_by_city", {
181
+ city: "New York, NY",
182
+ radius_miles: 15,
183
+ page: 1,
184
+ });
185
+ assert.ok(!isError, `search_events_by_city (optional) should not error: ${text}`);
186
+ assert.ok(typeof text === "string", "should return text");
187
+ });
188
+ it("search_events_by_city – with page_size", async () => {
189
+ const { text, isError } = await callTool("search_events_by_city", {
190
+ city: "Detroit, MI",
191
+ radius_miles: 25,
192
+ page: 1,
193
+ page_size: 10,
194
+ });
195
+ assert.ok(!isError, `search_events_by_city (page_size) should not error: ${text}`);
196
+ assert.ok(typeof text === "string", "should return text");
197
+ });
198
+ it("search_stores – no args (list first page)", async () => {
199
+ const { text, isError } = await callTool("search_stores", {
200
+ page: 1,
201
+ page_size: 5,
202
+ });
203
+ assert.ok(!isError, `search_stores should not error: ${text}`);
204
+ assert.ok(typeof text === "string", "should return text");
205
+ });
206
+ it("search_stores – by name", async () => {
207
+ const { text, isError } = await callTool("search_stores", {
208
+ search: "game",
209
+ page: 1,
210
+ page_size: 5,
211
+ });
212
+ assert.ok(!isError, `search_stores (search) should not error: ${text}`);
213
+ assert.ok(typeof text === "string", "should return text");
214
+ });
215
+ it("search_stores – by location", async () => {
216
+ const { text, isError } = await callTool("search_stores", {
217
+ latitude: 42.33,
218
+ longitude: -83.05,
219
+ radius_miles: 10,
220
+ page: 1,
221
+ page_size: 5,
222
+ });
223
+ assert.ok(!isError, `search_stores (location) should not error: ${text}`);
224
+ assert.ok(typeof text === "string", "should return text");
225
+ });
226
+ it("search_stores_by_city – required only", async () => {
227
+ const { text, isError } = await callTool("search_stores_by_city", {
228
+ city: "Detroit, MI",
229
+ });
230
+ assert.ok(!isError, `search_stores_by_city should not error: ${text}`);
231
+ assert.ok(typeof text === "string", "should return text");
232
+ });
233
+ it("search_stores_by_city – with optional params", async () => {
234
+ const { text, isError } = await callTool("search_stores_by_city", {
235
+ city: "Chicago, IL",
236
+ radius_miles: 20,
237
+ page: 1,
238
+ });
239
+ assert.ok(!isError, `search_stores_by_city (optional) should not error: ${text}`);
240
+ assert.ok(typeof text === "string", "should return text");
241
+ });
242
+ it("search_stores_by_city – with page_size", async () => {
243
+ const { text, isError } = await callTool("search_stores_by_city", {
244
+ city: "Detroit, MI",
245
+ radius_miles: 25,
246
+ page: 1,
247
+ page_size: 10,
248
+ });
249
+ assert.ok(!isError, `search_stores_by_city (page_size) should not error: ${text}`);
250
+ assert.ok(typeof text === "string", "should return text");
251
+ });
252
+ it("search_events – with status past", async () => {
253
+ const { text, isError } = await callTool("search_events", {
254
+ latitude: 42.33,
255
+ longitude: -83.05,
256
+ radius_miles: 100,
257
+ statuses: ["past"],
258
+ page: 1,
259
+ page_size: 5,
260
+ });
261
+ assert.ok(!isError, `search_events (status past) should not error: ${text}`);
262
+ assert.ok(typeof text === "string", "should return text");
263
+ });
264
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { formatRegistrationEntry } from "../lib/formatters.js";
4
+ describe("formatRegistrationEntry", () => {
5
+ it("formats entry with display_name", () => {
6
+ const entry = { display_name: "Alice" };
7
+ const out = formatRegistrationEntry(entry, 0);
8
+ assert.ok(out.startsWith("1. Alice"));
9
+ });
10
+ it("falls back to username", () => {
11
+ const entry = { username: "bob123" };
12
+ const out = formatRegistrationEntry(entry, 1);
13
+ assert.ok(out.includes("2. bob123"));
14
+ });
15
+ it("uses user.display_name when present", () => {
16
+ const entry = { user: { display_name: "Charlie Z" } };
17
+ const out = formatRegistrationEntry(entry, 2);
18
+ assert.ok(out.includes("3. Charlie Z"));
19
+ });
20
+ it("uses user first_name and last_name when present", () => {
21
+ const entry = {
22
+ user: { first_name: "Dana", last_name: "Smith" },
23
+ };
24
+ const out = formatRegistrationEntry(entry, 3);
25
+ assert.ok(out.includes("4. Dana Smith"));
26
+ });
27
+ it("includes status and registered_at when present", () => {
28
+ const entry = {
29
+ display_name: "Eve",
30
+ status: "confirmed",
31
+ registered_at: "2025-01-15T10:00:00Z",
32
+ };
33
+ const out = formatRegistrationEntry(entry, 4);
34
+ assert.ok(out.includes("Status: confirmed"));
35
+ assert.ok(out.includes("Registered:"));
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { formatStandingEntry } from "../lib/formatters.js";
4
+ describe("formatStandingEntry", () => {
5
+ it("formats entry with rank and player_name", () => {
6
+ const entry = { rank: 1, player_name: "Alice" };
7
+ const out = formatStandingEntry(entry, 0);
8
+ assert.ok(out.startsWith("1. Alice"));
9
+ });
10
+ it("falls back to placement and display_name", () => {
11
+ const entry = { placement: 2, display_name: "Bob" };
12
+ const out = formatStandingEntry(entry, 1);
13
+ assert.ok(out.includes("2. Bob"));
14
+ });
15
+ it("uses index when rank/placement missing", () => {
16
+ const entry = { player_name: "Charlie" };
17
+ const out = formatStandingEntry(entry, 2);
18
+ assert.ok(out.startsWith("3. Charlie"));
19
+ });
20
+ it("includes record when wins/losses present", () => {
21
+ const entry = {
22
+ rank: 1,
23
+ player_name: "Dana",
24
+ wins: 3,
25
+ losses: 1,
26
+ };
27
+ const out = formatStandingEntry(entry, 0);
28
+ assert.ok(out.includes("Record: 3-1"));
29
+ });
30
+ it("includes match_points and percentages when present", () => {
31
+ const entry = {
32
+ rank: 1,
33
+ player_name: "Eve",
34
+ match_points: 9,
35
+ opponent_match_win_pct: 0.6,
36
+ game_win_pct: 0.75,
37
+ };
38
+ const out = formatStandingEntry(entry, 0);
39
+ assert.ok(out.includes("Match points: 9"));
40
+ assert.ok(out.includes("OMWP: 60.0%"));
41
+ assert.ok(out.includes("GWP: 75.0%"));
42
+ });
43
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MCP tools for event search, details, registrations, and tournament standings.
3
+ */
4
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ export declare function registerEventTools(server: McpServer): void;