Skip to main content
Glama

BoardGameGeek MCP Server

by attilad
tools.ts14.9 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import * as db from './database.js'; import * as sync from './sync.js'; import * as vectorSearch from './vectorSearch.js'; // Type for the response that tools should return (matches MCP SDK expected format) type ToolContent = { type: "text"; text: string; } | { type: "image"; data: string; mimeType: string; } | { type: "audio"; data: string; mimeType: string; } | { type: "resource"; resource: { text: string; uri: string; mimeType?: string; } | { uri: string; blob: string; mimeType?: string; }; }; type ToolResponse = { content: ToolContent[]; isError?: boolean; [key: string]: unknown; }; // Register all tools with the MCP server export function registerTools(server: McpServer) { // Search games (uses local database first, then falls back to API) server.tool("search-games", "Search for board games by name", { query: z.string().describe("The name of the game to search for"), exact: z.boolean().optional().describe("Whether to search for an exact match (default: false)"), useLocalOnly: z.boolean().optional().describe("Whether to only search locally without API fallback (default: false)"), }, async (args, extra) => { try { // First try to search the local database const localResults = await db.searchGames(args.query); // If we have results or useLocalOnly is true, return them if (localResults.length > 0 || args.useLocalOnly) { return { content: [ { type: "text", text: JSON.stringify(localResults, null, 2), }, ], }; } // Otherwise, fall back to the BGG API // This will use the original implementation from index.js return { content: [ { type: "text", text: "No local results found. Would fall back to BGG API in the original implementation.", }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error searching for games: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // Get game details (uses local database first, then falls back to API) server.tool("get-game-details", "Get detailed information about a specific board game", { id: z.number().describe("The BoardGameGeek ID of the game"), stats: z.boolean().optional().describe("Whether to include ranking and rating stats (default: false)"), forceRefresh: z.boolean().optional().describe("Whether to force a refresh from the BGG API (default: false)"), }, async (args, extra) => { try { // Check if we need to force a refresh or if the game needs refreshing if (args.forceRefresh || await db.gameNeedsRefresh(args.id)) { await sync.syncGameDetails(args.id); } // Try to get the game from the local database const game = await db.getGame(args.id); // If we have the game, return it if (game) { return { content: [ { type: "text", text: JSON.stringify(game, null, 2), }, ], }; } // Otherwise, return an error return { isError: true, content: [ { type: "text", text: `Game with ID ${args.id} not found.`, }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error retrieving game details: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // Get user collection (uses local database first, then falls back to API) server.tool("get-user-collection", "Get a user's board game collection", { username: z.string().describe("The BoardGameGeek username"), owned: z.boolean().optional().describe("Filter to only show owned games (default: false)"), played: z.boolean().optional().describe("Filter to only show played games (default: false)"), rated: z.boolean().optional().describe("Filter to only show rated games (default: false)"), forceRefresh: z.boolean().optional().describe("Whether to force a refresh from the BGG API (default: false)"), }, async (args, extra) => { try { // Check if we need to force a refresh if (args.forceRefresh) { const success = await sync.syncUserCollection(args.username); if (!success) { return { content: [ { type: "text", text: JSON.stringify({ status: "queued", message: "Collection request has been queued by BoardGameGeek. Please try again in a few moments.", collection: [] }) }, ], }; } } // Get the collection from the local database let collection = await db.getUserCollection(args.username); // Apply filters if specified if (args.owned !== undefined) { collection = collection.filter((item: any) => item.own === (args.owned ? 1 : 0)); } if (args.played !== undefined) { collection = collection.filter((item: any) => item.played === (args.played ? 1 : 0)); } if (args.rated !== undefined) { collection = collection.filter((item: any) => args.rated ? item.rating !== null : true); } // If we have results, return them if (collection.length > 0) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", collection }) }, ], }; } // If no local results, try to sync from the API if (!args.forceRefresh) { const success = await sync.syncUserCollection(args.username); if (!success) { return { content: [ { type: "text", text: JSON.stringify({ status: "queued", message: "Collection request has been queued by BoardGameGeek. Please try again in a few moments.", collection: [] }) }, ], }; } // Try to get the collection again after syncing collection = await db.getUserCollection(args.username); // Apply filters again if (args.owned !== undefined) { collection = collection.filter((item: any) => item.own === (args.owned ? 1 : 0)); } if (args.played !== undefined) { collection = collection.filter((item: any) => item.played === (args.played ? 1 : 0)); } if (args.rated !== undefined) { collection = collection.filter((item: any) => args.rated ? item.rating !== null : true); } if (collection.length > 0) { return { content: [ { type: "text", text: JSON.stringify({ status: "success", collection }) }, ], }; } } // If still no results, return an empty collection return { content: [ { type: "text", text: JSON.stringify({ status: "success", message: `No games found in ${args.username}'s collection matching the specified filters.`, collection: [] }) }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: JSON.stringify({ status: "error", message: `Error retrieving user collection: ${error instanceof Error ? error.message : String(error)}`, collection: [] }) }, ], }; } }); // Get user plays (uses local database first, then falls back to API) server.tool("get-user-plays", "Get a user's recent board game plays", { username: z.string().describe("The BoardGameGeek username"), maxPlays: z.number().optional().describe("Maximum number of plays to return (default: 10)"), forceRefresh: z.boolean().optional().describe("Whether to force a refresh from the BGG API (default: false)"), }, async (args, extra) => { try { // Check if we need to force a refresh if (args.forceRefresh) { await sync.syncUserPlays(args.username, args.maxPlays || 10); } // Get the plays from the local database let plays = await db.getUserPlays(args.username, args.maxPlays || 10); // If we have results, return them if (plays.length > 0) { return { content: [ { type: "text", text: JSON.stringify(plays, null, 2), }, ], }; } // If no local results, try to sync from the API if (!args.forceRefresh) { await sync.syncUserPlays(args.username, args.maxPlays || 10); // Try to get the plays again after syncing plays = await db.getUserPlays(args.username, args.maxPlays || 10); if (plays.length > 0) { return { content: [ { type: "text", text: JSON.stringify(plays, null, 2), }, ], }; } } // If still no results, return an appropriate message return { content: [ { type: "text", text: `No plays found for user ${args.username}.`, }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error retrieving user plays: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // Get hot games (uses local database first, then falls back to API) server.tool("get-hot-games", "Get the current hottest board games on BoardGameGeek", { forceRefresh: z.boolean().optional().describe("Whether to force a refresh from the BGG API (default: false)"), }, async (args, extra) => { try { // Always sync hot games since they change frequently await sync.syncHotGames(); // Get the hot games from the database const hotGames = await db.getHotGames(); // Return the actual hot games list, not just a confirmation return { content: [ { type: "text", text: JSON.stringify(hotGames, null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error retrieving hot games: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // NEW TOOL: Get similar games server.tool("get-similar-games", "Get games similar to a specified game", { id: z.number().describe("The BoardGameGeek ID of the reference game"), limit: z.number().optional().describe("Maximum number of similar games to return (default: 10)"), }, async (args, extra) => { try { // Get the game from the database const game = await db.getGame(args.id); if (!game) { // If the game isn't in the database, sync it const success = await sync.syncGameDetails(args.id); if (!success) { return { content: [ { type: "text", text: `No game found with ID ${args.id}.`, }, ], }; } } // Find similar games const similarGames = await vectorSearch.findSimilarGames(args.id, args.limit || 10); // Return the results return { content: [ { type: "text", text: JSON.stringify(similarGames, null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error finding similar games: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // Sync user collection server.tool("sync-user-collection", "Synchronize a user's collection from BoardGameGeek", { username: z.string().describe("The BoardGameGeek username"), }, async (args, extra) => { try { const success = await sync.syncUserCollection(args.username); if (!success) { return { content: [ { type: "text", text: "Collection request has been queued by BoardGameGeek. Please try again in a few moments.", }, ], }; } // Get the actual collection data after synchronization const collection = await db.getUserCollection(args.username); return { content: [ { type: "text", text: JSON.stringify(collection, null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error synchronizing user collection: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); // Sync user plays server.tool("sync-user-plays", "Synchronize a user's plays from BoardGameGeek", { username: z.string().describe("The BoardGameGeek username"), maxPlays: z.number().optional().describe("Maximum number of plays to sync (default: 100)"), }, async (args, extra) => { try { await sync.syncUserPlays(args.username, args.maxPlays || 100); // Get the actual plays data after synchronization const plays = await db.getUserPlays(args.username, args.maxPlays || 100); return { content: [ { type: "text", text: JSON.stringify(plays, null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error synchronizing user plays: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/attilad/bgg-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server