#!/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);
});