UNPKG

unofficial-ravensburger-playhub-mcp

Version:
478 lines (477 loc) 23.7 kB
/** * Integration tests: spawn the Lorcana Event Finder MCP server and run each tool * with one or more call variations to ensure the server and all tools work end-to-end. * * Run after build: npm run build && npm test * Requires network (Ravensburger API and Play Hub geocoding/places APIs). */ import { describe, it, before, after } from "node:test"; import assert from "node:assert"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { Client } from "@modelcontextprotocol/sdk/client"; // StdioClientTransport is not re-exported from the client entry; load from SDK dist. import { StdioClientTransport } from "../../node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); // When run from dist/test/, go up to repo root const projectRoot = resolve(__dirname, "..", ".."); const EXPECTED_TOOL_NAMES = [ "list_capabilities", "list_filters", "list_quick_filters", "list_games", "get_game_details", "search_cards", "get_card_details", "search_places", "get_place_coordinates", "search_events", "get_event_details", "get_tournament_round_standings", "get_round_matches", "get_event_standings", "get_player_leaderboard", "get_player_leaderboard_by_store", "get_event_registrations", "search_events_by_city", "get_store_events", "search_stores", "get_store_details", "search_stores_by_city", ]; const parsedIntegrationTimeoutMs = Number.parseInt(process.env.INTEGRATION_TEST_TIMEOUT_MS ?? "", 10); const INTEGRATION_TIMEOUT_MS = Number.isFinite(parsedIntegrationTimeoutMs) && parsedIntegrationTimeoutMs > 0 ? parsedIntegrationTimeoutMs : 180_000; describe("MCP server integration – all tools and call variations", { timeout: INTEGRATION_TIMEOUT_MS }, () => { let client; let transport; before(async () => { transport = new StdioClientTransport({ command: "node", args: [resolve(projectRoot, "dist/index.js")], cwd: projectRoot, stderr: "pipe", }); client = new Client({ name: "mcp-tools-integration-test", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport); }); after(async () => { await transport.close(); }); it("lists all expected tools", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name).sort(); const expected = [...EXPECTED_TOOL_NAMES].sort(); assert.deepStrictEqual(names, expected, `Expected tools ${expected.join(", ")}; got ${names.join(", ")}`); }); async function callTool(name, args) { const result = await client.callTool({ name, arguments: args }); assert.ok(result, "callTool should return a result"); if ("content" in result && Array.isArray(result.content)) { const textPart = result.content.find((c) => c.type === "text" && "text" in c); return { content: result.content, text: textPart && "text" in textPart ? textPart.text : "", isError: "isError" in result ? !!result.isError : false, }; } return { content: result, text: "", isError: false }; } it("list_capabilities – no args", async () => { const { text, isError } = await callTool("list_capabilities", {}); assert.ok(!isError, `list_capabilities should not error: ${text}`); assert.ok(text.includes("search_events"), "capabilities should mention search_events"); assert.ok(text.includes("search_events_by_city"), "capabilities should mention search_events_by_city"); assert.ok(text.includes("get_event_details") || text.includes("get_event"), "capabilities should mention event details"); assert.ok(text.includes("get_player_leaderboard"), "capabilities should mention get_player_leaderboard"); assert.ok(text.includes("list_filters"), "capabilities should mention list_filters"); }); it("list_filters – no args", async () => { const { text, isError } = await callTool("list_filters", {}); assert.ok(!isError, `list_filters should not error: ${text}`); assert.ok(text.includes("Formats") || text.includes("formats"), "list_filters should include formats"); assert.ok(text.includes("Categories") || text.includes("categories"), "list_filters should include categories"); assert.ok(text.includes("Example") || text.includes("formats"), "list_filters should include usage example or param names"); }); it("list_quick_filters – default game", async () => { const { text, isError } = await callTool("list_quick_filters", {}); assert.ok(!isError, `list_quick_filters should not error: ${text}`); assert.ok(text.includes("Quick filters") || text.includes("No quick filters"), "should return quick-filter text"); }); it("list_games – no args", async () => { const { text, isError } = await callTool("list_games", {}); assert.ok(!isError, `list_games should not error: ${text}`); assert.ok(text.includes("slug") || text.includes("Available games"), "list_games should mention slugs/games"); }); it("get_game_details – disney-lorcana", async () => { const { text, isError } = await callTool("get_game_details", { game_slug: "disney-lorcana" }); assert.ok(!isError, `get_game_details should not error: ${text}`); assert.ok(text.includes("disney-lorcana") || text.includes("Disney Lorcana"), "should include game slug/name"); }); it("search_cards and get_card_details – card flow", async () => { const search = await callTool("search_cards", { query: "elsa", max_results: 5 }); assert.ok(!search.isError, `search_cards should not error: ${search.text}`); assert.ok(search.text.includes("Card ID") || search.text.includes("No cards"), "search_cards should include card IDs or no-results text"); const match = search.text.match(/Card ID: ([0-9a-fA-F-]{16,})/); if (!match) return; const details = await callTool("get_card_details", { card_id: match[1] }); assert.ok(!details.isError, `get_card_details should not error: ${details.text}`); assert.ok(details.text.includes("Card ID") || details.text.includes("Card:"), "get_card_details should include card metadata"); }); it("search_places – city autocomplete", async () => { const { text, isError } = await callTool("search_places", { query: "Detroit, MI", max_results: 5, }); assert.ok(!isError, `search_places should not error: ${text}`); assert.ok(text.includes("place_id:") || text.includes("No place suggestions"), "search_places should include place IDs or no-results text"); }); it("get_place_coordinates – from search_places place_id", async () => { const search = await callTool("search_places", { query: "Detroit, MI", max_results: 5, }); assert.ok(!search.isError, `search_places for get_place_coordinates should not error: ${search.text}`); const match = search.text.match(/place_id: ([A-Za-z0-9_-]{8,})/); if (!match) return; const geocoded = await callTool("get_place_coordinates", { place_id: match[1] }); assert.ok(!geocoded.isError, `get_place_coordinates should not error: ${geocoded.text}`); assert.ok(geocoded.text.includes("Latitude:") || geocoded.text.includes("Address:"), "get_place_coordinates should include address and coordinates"); }); it("search_events – required only", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, }); assert.ok(!isError, `search_events should not error: ${text}`); assert.ok(typeof text === "string" && text.length > 0, "should return non-empty text"); }); it("search_events – with optional params", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, radius_miles: 10, page: 1, page_size: 5, statuses: ["upcoming", "inProgress"], featured_only: false, }); assert.ok(!isError, `search_events (optional) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events – with text_search", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, radius_miles: 50, text_search: "Lorcana", page: 1, page_size: 5, }); assert.ok(!isError, `search_events (text_search) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events – with store_id", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, radius_miles: 100, store_id: 1, page: 1, page_size: 5, }); assert.ok(!isError, `search_events (store_id) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events – with start_date", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, start_date: "2025-01-01", page: 1, page_size: 5, }); assert.ok(!isError, `search_events (start_date) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events – invalid date returns error", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, start_date: "2026-13-40", }); assert.ok(isError, "should error for invalid date"); assert.ok(text.includes("YYYY-MM-DD"), "message should mention date format"); }); it("search_events – invalid format returns error", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, formats: ["NonExistentFormatName"], }); assert.ok(isError, "should error for unknown format"); assert.ok(text.includes("Unknown format") || text.includes("list_filters"), "message should mention format or list_filters"); }); it("get_event_details – valid-looking id", async () => { const { text, isError } = await callTool("get_event_details", { event_id: 1 }); // API may return 404 for id=1; server still returns content (possibly error message) assert.ok(typeof text === "string" && text.length > 0, "should return some content"); }); it("get_tournament_round_standings – required only", async () => { const { text, isError } = await callTool("get_tournament_round_standings", { round_id: 414976, }); assert.ok(!isError, `get_tournament_round_standings should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_tournament_round_standings – with pagination", async () => { const { text, isError } = await callTool("get_tournament_round_standings", { round_id: 414976, page: 1, page_size: 10, }); assert.ok(!isError, `get_tournament_round_standings (pagination) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_round_matches – required only", async () => { const { text, isError } = await callTool("get_round_matches", { round_id: 414976 }); assert.ok(!isError, `get_round_matches should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_event_standings – event with rounds", async () => { const { text, isError } = await callTool("get_event_standings", { event_id: 362750, page_size: 25, }); assert.ok(!isError, `get_event_standings should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); // Event 362750 has standings; response should include standings or a clear "no standings" message assert.ok(text.includes("362750") || text.includes("Standings") || text.includes("No standings"), "response should mention event or standings"); }); it("get_event_registrations – required only", async () => { const { text, isError } = await callTool("get_event_registrations", { event_id: 333450, }); assert.ok(!isError, `get_event_registrations should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_event_registrations – with pagination", async () => { const { text, isError } = await callTool("get_event_registrations", { event_id: 333450, page: 1, page_size: 10, }); assert.ok(!isError, `get_event_registrations (pagination) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_player_leaderboard – invalid date range returns error", async () => { const { text, isError } = await callTool("get_player_leaderboard", { city: "Detroit, MI", start_date: "2026-02-01", end_date: "2026-01-01", }); assert.ok(isError, "should error when start_date is after end_date"); assert.ok(text.includes("start_date") || text.includes("end_date") || text.includes("before"), "message should mention dates"); }); it("get_player_leaderboard – valid params returns content", async () => { const { text, isError } = await callTool("get_player_leaderboard", { city: "Detroit, MI", start_date: "2025-01-01", end_date: "2025-01-31", limit: 5, }); assert.ok(!isError, `get_player_leaderboard should not error: ${text}`); assert.ok(typeof text === "string" && text.length > 0, "should return text"); assert.ok(text.includes("Leaderboard") || text.includes("events") || text.includes("No past events"), "should mention leaderboard or events"); }); it("get_player_leaderboard_by_store – invalid date range returns error", async () => { const { text, isError } = await callTool("get_player_leaderboard_by_store", { store_id: 4622, start_date: "2026-02-01", end_date: "2026-01-01", }); assert.ok(isError, "should error when start_date is after end_date"); assert.ok(text.includes("start_date") || text.includes("end_date") || text.includes("before"), "message should mention dates"); }); it("get_player_leaderboard_by_store – valid params returns content", async () => { const { text, isError } = await callTool("get_player_leaderboard_by_store", { store_id: 4622, start_date: "2025-01-01", end_date: "2025-01-31", limit: 5, }); assert.ok(!isError, `get_player_leaderboard_by_store should not error: ${text}`); assert.ok(typeof text === "string" && text.length > 0, "should return text"); assert.ok(text.includes("Leaderboard") || text.includes("events") || text.includes("No past events") || text.includes("Store"), "should mention leaderboard, events, or store"); }); it("get_player_leaderboard_by_store – invalid format returns error", async () => { const { text, isError } = await callTool("get_player_leaderboard_by_store", { store_id: 4622, start_date: "2025-01-01", end_date: "2025-01-31", formats: ["NonExistentFormatName"], }); assert.ok(isError, "should error for unknown format"); assert.ok(text.includes("Unknown format") || text.includes("list_filters"), "message should mention format or list_filters"); }); it("search_events_by_city – required only", async () => { const { text, isError } = await callTool("search_events_by_city", { city: "Detroit, MI", }); assert.ok(!isError, `search_events_by_city should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events_by_city – with optional params", async () => { const { text, isError } = await callTool("search_events_by_city", { city: "New York, NY", radius_miles: 15, page: 1, }); assert.ok(!isError, `search_events_by_city (optional) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events_by_city – with page_size", async () => { const { text, isError } = await callTool("search_events_by_city", { city: "Detroit, MI", radius_miles: 25, page: 1, page_size: 10, }); assert.ok(!isError, `search_events_by_city (page_size) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events_by_city – invalid date returns error", async () => { const { text, isError } = await callTool("search_events_by_city", { city: "Detroit, MI", start_date: "bad-date", }); assert.ok(isError, "should error for invalid date"); assert.ok(text.includes("YYYY-MM-DD"), "message should mention date format"); }); it("get_store_events – required only", async () => { // Use a known store ID for testing const { text, isError } = await callTool("get_store_events", { store_id: 4622, }); assert.ok(!isError, `get_store_events should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_store_events – with optional params", async () => { const { text, isError } = await callTool("get_store_events", { store_id: 4622, statuses: ["past"], page: 1, page_size: 5, }); assert.ok(!isError, `get_store_events (optional) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_store_events – invalid category returns error", async () => { const { text, isError } = await callTool("get_store_events", { store_id: 4622, categories: ["NonExistentCategoryName"], }); assert.ok(isError, "should error for unknown category"); assert.ok(text.includes("Unknown category") || text.includes("list_filters"), "message should mention category or list_filters"); }); it("search_stores – no args (list first page)", async () => { const { text, isError } = await callTool("search_stores", { page: 1, page_size: 5, }); assert.ok(!isError, `search_stores should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("get_store_details – from search_stores game_store_id", async () => { const search = await callTool("search_stores", { page: 1, page_size: 5 }); assert.ok(!search.isError, `search_stores for get_store_details should not error: ${search.text}`); const match = search.text.match(/Game Store ID: ([0-9a-fA-F-]{16,})/); if (!match) return; const details = await callTool("get_store_details", { game_store_id: match[1] }); assert.ok(!details.isError, `get_store_details should not error: ${details.text}`); assert.ok(details.text.includes("Game Store ID") || details.text.includes("Store ID"), "store details should include IDs"); }); it("search_stores – by name", async () => { const { text, isError } = await callTool("search_stores", { search: "game", page: 1, page_size: 5, }); assert.ok(!isError, `search_stores (search) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores – with store_type filter", async () => { const { text, isError } = await callTool("search_stores", { search: "game", store_type: "organizedPlay", page: 1, page_size: 5, }); assert.ok(!isError, `search_stores (store_type) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores – by location", async () => { const { text, isError } = await callTool("search_stores", { latitude: 42.33, longitude: -83.05, radius_miles: 10, page: 1, page_size: 5, }); assert.ok(!isError, `search_stores (location) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores_by_city – required only", async () => { const { text, isError } = await callTool("search_stores_by_city", { city: "Detroit, MI", }); assert.ok(!isError, `search_stores_by_city should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores_by_city – with optional params", async () => { const { text, isError } = await callTool("search_stores_by_city", { city: "Chicago, IL", radius_miles: 20, page: 1, }); assert.ok(!isError, `search_stores_by_city (optional) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores_by_city – with store_type filter", async () => { const { text, isError } = await callTool("search_stores_by_city", { city: "Chicago, IL", radius_miles: 20, store_type: "organizedPlay", page: 1, page_size: 5, }); assert.ok(!isError, `search_stores_by_city (store_type) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_stores_by_city – with page_size", async () => { const { text, isError } = await callTool("search_stores_by_city", { city: "Detroit, MI", radius_miles: 25, page: 1, page_size: 10, }); assert.ok(!isError, `search_stores_by_city (page_size) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); it("search_events – with status past", async () => { const { text, isError } = await callTool("search_events", { latitude: 42.33, longitude: -83.05, radius_miles: 100, statuses: ["past"], page: 1, page_size: 5, }); assert.ok(!isError, `search_events (status past) should not error: ${text}`); assert.ok(typeof text === "string", "should return text"); }); });