UNPKG

mcp-grocy

Version:

Model Context Protocol (MCP) server for Grocy integration

164 lines (163 loc) 7.08 kB
/** * Simplified MCP Server implementation * Reduced complexity and improved performance */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js'; import { createToolRegistry } from '../tools/index.js'; import { config } from '../config/index.js'; import { startHttpServer } from './http-server.js'; import { ResourceHandler } from './resources.js'; import { logger } from '../utils/logger.js'; import { ErrorHandler } from '../utils/errors.js'; export class GrocyMcpServer { server; enabledTools = new Set(); toolSubConfigs = new Map(); toolAckTokens = new Map(); resourceHandler; toolRegistry; constructor(server, toolRegistry, resourceHandler) { this.server = server; this.toolRegistry = toolRegistry; this.resourceHandler = resourceHandler; this.parseToolConfiguration(); this.setupHandlers(); this.setupErrorHandling(); } static async create() { // Initialize components const [toolRegistry, resourceHandler] = await Promise.all([ createToolRegistry(), Promise.resolve(new ResourceHandler()) ]); // Create server const server = new Server({ name: SERVER_NAME, version: VERSION, serverUrl: "https://github.com/miguelangel-nubla/mcp-grocy", documentationUrl: "https://github.com/miguelangel-nubla/mcp-grocy/blob/main/README.md" }, { capabilities: { tools: {}, resources: {}, prompts: {} } }); return new GrocyMcpServer(server, toolRegistry, resourceHandler); } parseToolConfiguration() { const { enabledTools, toolSubConfigs, toolAckTokens } = config.parseToolConfiguration(); this.toolSubConfigs = toolSubConfigs; this.toolAckTokens = toolAckTokens; // Validate tool names const validToolNames = new Set(this.toolRegistry.getToolNames()); if (enabledTools.size > 0) { const invalidTools = Array.from(enabledTools).filter(tool => !validToolNames.has(tool)); if (invalidTools.length > 0) { const validNames = Array.from(validToolNames).sort().join(', '); logger.error(`Invalid tools: ${invalidTools.join(', ')}. Valid: ${validNames}`, 'CONFIG'); process.exit(1); } this.enabledTools = enabledTools; logger.config(`Enabled tools: ${Array.from(enabledTools).join(', ')}`); } else { logger.warn('No tools enabled', 'CONFIG'); } } setupHandlers() { // Initialize handler this.server.setRequestHandler(z.object({ method: z.literal('initialize'), params: z.any().optional() }), async (request) => { logger.debug('Initialize request', 'MCP'); return { protocolVersion: request.params?.protocolVersion || '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {} }, serverInfo: { name: SERVER_NAME, version: VERSION } }; }); // List tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { const allTools = this.toolRegistry.getDefinitions(); const filteredTools = allTools.filter(tool => this.enabledTools.has(tool.name)); logger.config(`Available tools: ${filteredTools.map(t => t.name).join(', ')}`); return { tools: filteredTools }; }); // Call tool this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name: toolName, arguments: args } = request.params; // Check if tool is enabled if (!this.enabledTools.has(toolName)) { throw new McpError(ErrorCode.InvalidRequest, `Tool '${toolName}' is not enabled. Enable it in your configuration.`); } // Get handler const handler = this.toolRegistry.getHandler(toolName); if (!handler) { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`); } try { const subConfigs = this.toolSubConfigs.get(toolName); const result = await handler(args, subConfigs); // Automatically add ack_token to successful responses (at the beginning) if (!result.isError) { const ackToken = this.toolAckTokens.get(toolName); if (ackToken) { result.content.unshift({ type: 'text', text: `Acknowledgment token: ${ackToken}` }); } } return result; } catch (error) { ErrorHandler.logError(error, `tool: ${toolName}`); throw ErrorHandler.toMcpError(error, `${toolName} failed`); } }); // Resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return this.resourceHandler.listResources(); }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { return this.resourceHandler.readResource(request.params.uri); }); } setupErrorHandling() { this.server.onerror = (error) => { logger.error('MCP protocol error', 'MCP', { error }); }; process.on('SIGINT', async () => { logger.info('Shutting down server...', 'SERVER'); await this.server.close(); process.exit(0); }); } async start() { // Start STDIO transport const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('MCP server running on stdio', 'SERVER'); // Start HTTP/SSE if enabled if (config.server.enable_http_server) { try { logger.config(`Starting HTTP server on port ${config.server.http_server_port}`); const serverFactory = () => this.server; await startHttpServer(serverFactory, config.server.http_server_port); } catch (error) { logger.error('Failed to start HTTP server', 'SERVER', { error }); logger.error('HTTP server is explicitly enabled but cannot start - exiting', 'SERVER'); process.exit(1); } } } // Expose server for HTTP transport get serverInstance() { return this.server; } } export default GrocyMcpServer;