howtocook-mcp
Version:
MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭
201 lines (200 loc) • 8.13 kB
JavaScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Command } from 'commander';
import { createServer } from 'http';
import { fetchRecipes, getAllCategories } from './data/recipes.js';
import { registerGetAllRecipesTool } from './tools/getAllRecipes.js';
import { registerGetRecipeByIdTool } from './tools/getRecipeById.js';
import { registerGetRecipesByCategoryTool } from './tools/getRecipesByCategory.js';
import { registerRecommendMealsTool } from './tools/recommendMeals.js';
import { registerWhatToEatTool } from './tools/whatToEat.js';
// 全局变量存储数据
let recipes = [];
let categories = [];
// 命令行参数处理
const program = new Command()
.option('--transport <stdio|http|sse>', 'transport type', 'stdio')
.option('--port <number>', 'port for HTTP/SSE transport', '3000')
.parse(process.argv);
const cliOptions = program.opts();
const allowedTransports = ['stdio', 'http', 'sse'];
if (!allowedTransports.includes(cliOptions.transport)) {
console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http, sse.`);
process.exit(1);
}
const TRANSPORT_TYPE = (cliOptions.transport || 'stdio');
const PORT = parseInt(cliOptions.port, 10);
const logInfo = (...args) => {
if (TRANSPORT_TYPE === 'stdio') {
console.error(...args);
}
else {
console.log(...args);
}
};
// SSE transports
const sseTransports = {};
// 创建MCP服务器实例
function createServerInstance() {
const server = new McpServer({
name: 'howtocook-mcp',
version: '0.2.2'
}, {
capabilities: {
logging: {}
}
});
// 注册所有工具
registerGetAllRecipesTool(server, recipes);
registerGetRecipesByCategoryTool(server, recipes, categories);
registerRecommendMealsTool(server, recipes);
registerWhatToEatTool(server, recipes);
registerGetRecipeByIdTool(server, recipes);
return server;
}
// 加载菜谱数据
function loadRecipeData() {
try {
recipes = fetchRecipes();
categories = getAllCategories(recipes);
logInfo(`Loaded ${recipes.length} recipes.`);
}
catch (error) {
console.error('加载菜谱数据失败:', error);
recipes = [];
categories = [];
throw error;
}
}
// 启动服务的主函数
async function main() {
// 加载菜谱数据
await loadRecipeData();
if (TRANSPORT_TYPE === 'http' || TRANSPORT_TYPE === 'sse') {
const httpServer = createServer(async (req, res) => {
const url = new URL(req.url || '', `http://${req.headers.host}`).pathname;
// 设置 CORS 头
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id, mcp-session-id');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
try {
if (url === '/mcp') {
// 为每个请求创建新的服务器实例
const requestServer = createServerInstance();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await requestServer.connect(transport);
await transport.handleRequest(req, res);
}
else if (url === '/sse' && req.method === 'GET') {
// Create new SSE transport for GET request
const requestServer = createServerInstance();
const sseTransport = new SSEServerTransport('/messages', res);
// Store the transport by session ID
sseTransports[sseTransport.sessionId] = sseTransport;
// Clean up transport when connection closes
res.on('close', () => {
delete sseTransports[sseTransport.sessionId];
});
await requestServer.connect(sseTransport);
}
else if (url === '/messages' && req.method === 'POST') {
// Get session ID from query parameters
const sessionId = new URL(req.url || '', `http://${req.headers.host}`).searchParams.get('sessionId') ?? '';
if (!sessionId) {
res.writeHead(400);
res.end('Missing sessionId parameter');
return;
}
// Get existing transport for this session
const sseTransport = sseTransports[sessionId];
if (!sseTransport) {
res.writeHead(400);
res.end(`No transport found for sessionId: ${sessionId}`);
return;
}
// Handle the POST message with the existing transport
await sseTransport.handlePostMessage(req, res);
}
else if (url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', transport: TRANSPORT_TYPE }));
}
else if (url === '/info') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
name: 'HowToCook MCP Server',
version: '0.2.2',
transport: TRANSPORT_TYPE,
endpoints: {
mcp: '/mcp',
sse: '/sse',
health: '/health',
info: '/info'
},
recipeCount: recipes.length
}));
}
else {
res.writeHead(404);
res.end('Not found');
}
}
catch (error) {
console.error('处理请求时出错:', error);
if (!res.headersSent) {
res.writeHead(500);
res.end('Internal Server Error');
}
}
});
httpServer.listen(PORT, () => {
logInfo(`HowToCook MCP ${TRANSPORT_TYPE.toUpperCase()} server started.`);
if (TRANSPORT_TYPE === 'http') {
logInfo(`MCP endpoint: http://localhost:${PORT}/mcp`);
}
else if (TRANSPORT_TYPE === 'sse') {
logInfo(`MCP endpoint: http://localhost:${PORT}/sse`);
}
logInfo(`Health: http://localhost:${PORT}/health`);
logInfo(`Info: http://localhost:${PORT}/info`);
});
}
else {
// stdio 模式
const server = createServerInstance();
const transport = new StdioServerTransport();
try {
await server.connect(transport);
logInfo('HowToCook MCP STDIO server started.');
}
catch (error) {
console.error('服务器启动失败:', error);
process.exit(1);
}
}
}
// 优雅关闭
process.on('SIGINT', async () => {
logInfo('\nShutting down server...');
process.exit(0);
});
process.on('SIGTERM', async () => {
logInfo('\nReceived termination signal, shutting down server...');
process.exit(0);
});
// 启动服务器
main().catch((error) => {
console.error('启动服务器失败:', error);
process.exit(1);
});