Skip to main content
Glama
WhenYouAreStrange

goodbook-mcp

index.js16.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import PDFParser from './pdf-parser.js'; // Initialize PDF parser const pdfParser = new PDFParser(); let isInitialized = false; // Create server instance const server = new Server( { name: "goodbook", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Helper function to ensure PDF parser is initialized async function ensureInitialized() { if (!isInitialized) { isInitialized = await pdfParser.initialize(); if (!isInitialized) { throw new Error("Could not initialize PDF parser. Please check if the menu.pdf file exists."); } } return isInitialized; } // Helper function for expanding search queries with synonyms function expandSearchQuery(query) { const synonyms = { // Синонимы для блюд 'котлеты': ['котлеты', 'биточки', 'зразы'], 'картофельные': ['картофельные', 'картофель', 'из картофеля'], 'морковные': ['морковные', 'морковь', 'из моркови', 'морковочные'], 'творог': ['творог', 'творожный', 'сыр творожный', 'кварк'], 'суп': ['суп', 'похлебка', 'бульон', 'борщ', 'щи', 'солянка'], 'пиво': ['пиво', 'пивной', 'с пивом', 'на пиве'], 'сладкий': ['сладкий', 'десертный', 'фруктовый'], 'плов': ['плов', 'рис с мясом', 'узбекский плов'], 'борщ': ['борщ', 'свекольный суп', 'украинский борщ'], 'паста': ['паста', 'макароны', 'спагетти', 'лапша'], 'тирамису': ['тирамису', 'итальянский десерт', 'маскарпоне'], 'пельмени': ['пельмени', 'вареники', 'манты', 'хинкали'], 'салат': ['салат', 'оливье', 'овощной'], 'десерт': ['десерт', 'сладкое', 'торт', 'пирог', 'выпечка'], // Синонимы для процессов 'приготовление': ['приготовление', 'готовка', 'варка', 'жарка', 'тушение'], 'температура': ['температура', 'градус', 'нагрев', 'охлаждение'], 'безопасность': ['безопасность', 'гигиена', 'санитария', 'чистота'], 'хранение': ['хранение', 'консервация', 'сохранность'], // Синонимы для ингредиентов 'мясо': ['мясо', 'говядина', 'свинина', 'баранина', 'курица'], 'овощи': ['овощи', 'морковь', 'лук', 'капуста', 'картофель'], 'специи': ['специи', 'приправы', 'пряности', 'соль', 'перец'] }; const queryWords = query.toLowerCase().split(/\s+/); const expandedQueries = [query]; // Включаем оригинальный запрос // Добавляем варианты с синонимами for (const word of queryWords) { if (synonyms[word]) { for (const synonym of synonyms[word]) { if (synonym !== word) { const newQuery = query.replace(new RegExp(word, 'gi'), synonym); if (!expandedQueries.includes(newQuery)) { expandedQueries.push(newQuery); } } } } } return expandedQueries; } // Zod schemas for input validation (defined first) const searchFoodStandardsSchema = z.object({ query: z.string().describe("Search term or phrase to look for in the food standards document"), section: z.string().optional().describe("Optional: specific section to search within"), }); const getCookingGuidelinesSchema = z.object({ dish_type: z.string().describe("Type of dish or cooking method to get guidelines for"), section: z.string().optional().describe("Optional: specific section to look in"), }); const getSectionContentSchema = z.object({ section_name: z.string().describe("Name of the section to retrieve content from"), limit: z.number().default(1000).describe("Maximum number of characters to return"), }); const getFoodSafetyInfoSchema = z.object({ topic: z.string().describe("Specific food safety topic (temperature, storage, hygiene, etc.)"), }); const findRecipeStandardsSchema = z.object({ dish_name: z.string().describe("Name of the dish to find recipe standards for"), cuisine_type: z.string().optional().describe("Optional: type of cuisine or cooking style"), }); // Helper function to convert zod schema to JSON schema format for MCP function zodToJsonSchema(zodSchema) { try { const shape = zodSchema._def.shape(); const properties = {}; const requiredFields = []; Object.keys(shape).forEach(key => { const field = shape[key]; const isOptional = field._def.typeName === "ZodOptional"; if (!isOptional) { requiredFields.push(key); } // Get the inner type for optional fields const actualField = isOptional ? field._def.innerType : field; properties[key] = { type: actualField._def.typeName === "ZodString" ? "string" : actualField._def.typeName === "ZodNumber" ? "number" : "string", description: actualField._def.description || "" }; // Handle default values for ZodDefault if (actualField._def.typeName === "ZodDefault") { const innerType = actualField._def.innerType; properties[key].type = innerType._def.typeName === "ZodNumber" ? "number" : "string"; properties[key].default = actualField._def.defaultValue(); properties[key].description = innerType._def.description || ""; } }); return { type: "object", properties, required: requiredFields }; } catch (error) { console.error("Error converting zod schema to JSON schema:", error); // Fallback to basic object schema return { type: "object", properties: {}, required: [] }; } } // Tool definitions const toolDefinitions = [ { name: "search_food_standards", description: "Search for specific food preparation standards, recipes, or cooking guidelines in the food service literature", inputSchema: zodToJsonSchema(searchFoodStandardsSchema) }, { name: "get_cooking_guidelines", description: "Get cooking guidelines and standards for specific dishes or cooking methods", inputSchema: zodToJsonSchema(getCookingGuidelinesSchema) }, { name: "list_sections", description: "List all available sections in the food service standards document", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "get_section_content", description: "Get content from a specific section of the food standards document", inputSchema: zodToJsonSchema(getSectionContentSchema) }, { name: "get_food_safety_info", description: "Get food safety information and hygiene standards", inputSchema: zodToJsonSchema(getFoodSafetyInfoSchema) }, { name: "find_recipe_standards", description: "Find standardized recipes and preparation methods for specific dishes", inputSchema: zodToJsonSchema(findRecipeStandardsSchema) } ]; // Set up handlers server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("Received list_tools request"); await ensureInitialized(); return { tools: toolDefinitions }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error(`Received call_tool request for: ${request.params.name}`); console.error(`Arguments:`, JSON.stringify(request.params.arguments, null, 2)); try { await ensureInitialized(); const args = request.params.arguments || {}; switch (request.params.name) { case "search_food_standards": { const { query, section } = searchFoodStandardsSchema.parse(args); return await searchFoodStandards(query, section); } case "get_cooking_guidelines": { const { dish_type, section } = getCookingGuidelinesSchema.parse(args); return await getCookingGuidelines(dish_type, section); } case "list_sections": { return await listSections(); } case "get_section_content": { const { section_name, limit } = getSectionContentSchema.parse(args); return await getSectionContent(section_name, limit); } case "get_food_safety_info": { const { topic } = getFoodSafetyInfoSchema.parse(args); return await getFoodSafetyInfo(topic); } case "find_recipe_standards": { const { dish_name, cuisine_type } = findRecipeStandardsSchema.parse(args); return await findRecipeStandards(dish_name, cuisine_type); } default: return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }] }; } } catch (error) { console.error(`Error executing tool ${request.params.name}:`, error); return { content: [{ type: "text", text: `Error executing tool: ${error.message}` }], isError: true }; } }); // Error handling server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on('SIGINT', async () => { console.error("Received SIGINT, shutting down gracefully..."); await server.close(); process.exit(0); }); // Tool implementation functions async function searchFoodStandards(query, section) { // Расширенный поиск с синонимами const expandedQueries = expandSearchQuery(query); let allResults = []; for (const searchQuery of expandedQueries) { const results = pdfParser.searchContent(searchQuery, section); if (results.results) { allResults.push(...results.results); } } // Удаляем дубликаты const uniqueResults = allResults.filter((result, index, self) => index === self.findIndex(r => r.content === result.content) ); let response = `Search results for "${query}"`; if (section) { response += ` in section "${section}"`; } response += `:\n\nFound ${uniqueResults.length} matches\n\n`; if (uniqueResults.length === 0) { response += "No matching content found."; } else { uniqueResults.slice(0, 15).forEach((result, index) => { response += `Result ${index + 1}:\n`; if (result.before) response += `Context: ${result.before}\n`; response += `> ${result.content}\n`; if (result.after) response += `Context: ${result.after}\n`; response += "\n"; }); } return { content: [{ type: "text", text: response }] }; } async function getCookingGuidelines(dish_type, section) { // Search for cooking-related terms const cookingTerms = [ dish_type, `приготовление ${dish_type}`, `готовка ${dish_type}`, `рецепт ${dish_type}`, `preparation ${dish_type}`, `cooking ${dish_type}`, `recipe ${dish_type}` ]; let allResults = []; for (const term of cookingTerms) { const results = pdfParser.searchContent(term, section); if (results.results) { allResults.push(...results.results); } } // Remove duplicates const uniqueResults = allResults.filter((result, index, self) => index === self.findIndex(r => r.content === result.content) ); let response = `Cooking guidelines for "${dish_type}":\n\n`; if (uniqueResults.length === 0) { response += "No specific cooking guidelines found for this dish type."; } else { uniqueResults.slice(0, 10).forEach((result, index) => { response += `Guideline ${index + 1}:\n`; response += `${result.content}\n\n`; }); } return { content: [{ type: "text", text: response }] }; } async function listSections() { const sections = pdfParser.getSections(); let response = "Available sections in the food standards document:\n\n"; sections.forEach((section, index) => { response += `${index + 1}. ${section.name}\n`; response += ` Content length: ${section.contentLength} items\n`; response += ` Preview: ${section.preview}\n\n`; }); return { content: [{ type: "text", text: response }] }; } async function getSectionContent(section_name, limit) { const content = pdfParser.getContent(section_name, limit); if (content.error) { return { content: [{ type: "text", text: `Error: ${content.error}` }] }; } return { content: [{ type: "text", text: `Content from section "${content.section}":\n\n${content.content}` }] }; } async function getFoodSafetyInfo(topic) { const safetyTerms = [ topic, `безопасность ${topic}`, `гигиена ${topic}`, `санитария ${topic}`, `температура ${topic}`, `хранение ${topic}`, `safety ${topic}`, `hygiene ${topic}`, `sanitation ${topic}`, `temperature ${topic}`, `storage ${topic}` ]; let allResults = []; for (const term of safetyTerms) { const results = pdfParser.searchContent(term); if (results.results) { allResults.push(...results.results); } } const uniqueResults = allResults.filter((result, index, self) => index === self.findIndex(r => r.content === result.content) ); let response = `Food safety information for "${topic}":\n\n`; if (uniqueResults.length === 0) { response += "No specific food safety information found for this topic."; } else { uniqueResults.slice(0, 8).forEach((result, index) => { response += `Safety guideline ${index + 1}:\n`; response += `${result.content}\n\n`; }); } return { content: [{ type: "text", text: response }] }; } async function findRecipeStandards(dish_name, cuisine_type) { const recipeTerms = [ dish_name, `рецепт ${dish_name}`, `стандарт ${dish_name}`, `приготовление ${dish_name}`, `recipe ${dish_name}`, `standard ${dish_name}`, `preparation ${dish_name}` ]; if (cuisine_type) { recipeTerms.push(`${cuisine_type} ${dish_name}`); } let allResults = []; for (const term of recipeTerms) { const results = pdfParser.searchContent(term); if (results.results) { allResults.push(...results.results); } } const uniqueResults = allResults.filter((result, index, self) => index === self.findIndex(r => r.content === result.content) ); let response = `Recipe standards for "${dish_name}"`; if (cuisine_type) response += ` (${cuisine_type} cuisine)`; response += ":\n\n"; if (uniqueResults.length === 0) { response += "No specific recipe standards found for this dish."; } else { uniqueResults.slice(0, 8).forEach((result, index) => { response += `Standard ${index + 1}:\n`; response += `${result.content}\n\n`; }); } return { content: [{ type: "text", text: response }] }; } // Running the server async function main() { console.error("Starting Goodbook MCP Server..."); console.error("Initializing PDF parser..."); try { await ensureInitialized(); console.error("PDF parser initialized successfully"); } catch (error) { console.error("Warning: Failed to initialize PDF parser:", error.message); console.error("Server will start but tools may not work properly"); } const transport = new StdioServerTransport(); console.error("Connecting to stdio transport..."); await server.connect(transport); console.error("Goodbook MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

Implementation Reference

Latest Blog Posts

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/WhenYouAreStrange/goodbook-mcp'

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