UNPKG

sap-b1-mcp-server

Version:

SAP Business One Service Layer MCP Server

408 lines 17.5 kB
#!/usr/bin/env bun import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema, CreateMessageRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { SAPClient } from './sap/client.js'; import { ConfigManager } from './utils/config.js'; import { logger, LogLevel } from './utils/logger.js'; import { StdioTransport } from './transports/stdio.js'; import { HttpTransport } from './transports/http.js'; import { StreamingHttpTransport } from './transports/streaming-http.js'; import { SessionManager } from './sap/session.js'; // Tool handlers import { AuthToolHandler, authTools } from './tools/auth.js'; import { MasterDataToolHandler, masterDataTools } from './tools/master-data.js'; import { DocumentToolHandler, documentTools } from './tools/documents.js'; import { QueryToolHandler, queryTools } from './tools/queries.js'; export class SAPMCPServer { server; configManager; sapClient = null; authHandler = null; masterDataHandler = null; documentHandler = null; queryHandler = null; transport = null; constructor() { this.configManager = new ConfigManager(); this.server = new Server({ name: 'sap-b1-mcp-server', version: '1.0.0' }, { capabilities: { tools: {}, sampling: {} } }); this.setupServerHandlers(); } /** * Initialize the server with configuration */ async initialize(configPath) { try { // Load configuration const config = await this.configManager.loadConfig(configPath); // Set log level based on environment if (config.environment === 'development') { logger.setLevel(LogLevel.DEBUG); } else if (config.environment === 'production') { logger.setLevel(LogLevel.INFO); } logger.info('Initializing SAP B1 MCP Server', { config: this.configManager.getConfigSummary() }); // Initialize SAP client this.sapClient = new SAPClient(config.sap); // Initialize tool handlers this.authHandler = new AuthToolHandler(this.sapClient); this.masterDataHandler = new MasterDataToolHandler(this.sapClient); this.documentHandler = new DocumentToolHandler(this.sapClient); this.queryHandler = new QueryToolHandler(this.sapClient); // Initialize transport if (config.transport.type === 'streaming-http' && config.transport.http) { const streamingOptions = { port: config.transport.http.port, host: config.transport.http.host, cors: config.transport.http.cors ?? true, apiKey: config.transport.http.apiKey, requireApiKey: config.transport.http.requireApiKey ?? true }; this.transport = new StreamingHttpTransport(this.server, this.sapClient, streamingOptions); } else if (config.transport.type === 'http' && config.transport.http) { const httpOptions = { port: config.transport.http.port, host: config.transport.http.host, cors: config.transport.http.cors ?? true }; this.transport = new HttpTransport(this.server, httpOptions); } else { this.transport = new StdioTransport(this.server); } logger.info('SAP B1 MCP Server initialized successfully'); } catch (error) { logger.error('Failed to initialize SAP B1 MCP Server', error); throw error; } } /** * Start the server */ async start() { if (!this.transport) { throw new Error('Server not initialized. Call initialize() first.'); } try { logger.info('Starting SAP B1 MCP Server'); await this.transport.start(); // Test SAP connection on startup for HTTP transport (non-blocking) if (this.configManager.isHttpTransport() && this.sapClient) { // Run SAP connection test in background without blocking startup setImmediate(async () => { try { logger.info('Testing SAP B1 connection in background...'); const testResult = await this.sapClient.testConnection(); if (testResult.success) { logger.info('SAP B1 connection test successful', testResult.sessionInfo); } else { logger.warn('SAP B1 connection test failed', { message: testResult.message }); } } catch (error) { logger.warn('Could not test SAP B1 connection on startup', error); } }); } } catch (error) { logger.error('Failed to start SAP B1 MCP Server', error); throw error; } } /** * Stop the server */ async stop() { try { logger.info('Stopping SAP B1 MCP Server'); // Logout from SAP if connected if (this.sapClient) { try { await this.sapClient.logout(); } catch (error) { logger.warn('Error during SAP logout', error); } } // Stop transport if (this.transport) { await this.transport.stop(); } // Reset session manager SessionManager.resetInstance(); logger.info('SAP B1 MCP Server stopped'); } catch (error) { logger.error('Error stopping SAP B1 MCP Server', error); } } /** * Setup server request handlers */ setupServerHandlers() { // Initialize handler - required by MCP protocol this.server.setRequestHandler(InitializeRequestSchema, async (request) => { logger.info('MCP initialization request received', { protocolVersion: request.params.protocolVersion, clientInfo: request.params.clientInfo }); return { protocolVersion: "2024-11-05", capabilities: { tools: {}, logging: {}, sampling: {} }, serverInfo: { name: "sap-b1-mcp-server", version: "1.0.0" } }; }); // List tools handler this.server.setRequestHandler(ListToolsRequestSchema, async () => { const allTools = [ ...authTools, ...masterDataTools, ...documentTools, ...queryTools ]; logger.debug('Listing available tools', { count: allTools.length }); return { tools: allTools }; }); // Sampling handler - required for MCP clients that use sampling this.server.setRequestHandler(CreateMessageRequestSchema, async (request) => { logger.info('MCP sampling request received', { messages: request.params.messages?.length || 0, modelPreferences: request.params.modelPreferences, systemPrompt: request.params.systemPrompt ? 'present' : 'none' }); // For now, return a simple response indicating sampling is not implemented // In a full implementation, this would forward to an LLM service return { model: "sap-b1-assistant", stopReason: "endTurn", role: "assistant", content: { type: "text", text: "Sampling capability is available but not fully implemented. This SAP B1 MCP server provides tools for SAP Business One integration. Please use the available tools instead of sampling." } }; }); // Call tool handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info('Tool execution started', { tool: name, args: JSON.stringify(args) }); const startTime = Date.now(); try { let result; // Route to appropriate handler switch (name) { // Authentication tools case 'sap_login': result = await this.authHandler.handleLogin(args); break; case 'sap_logout': result = await this.authHandler.handleLogout(args); break; case 'sap_session_status': result = await this.authHandler.handleSessionStatus(args); break; // Master data tools case 'sap_get_business_partners': result = await this.masterDataHandler.handleGetBusinessPartners(args); break; case 'sap_get_business_partner': result = await this.masterDataHandler.handleGetBusinessPartner(args); break; case 'sap_get_product_categories': result = await this.masterDataHandler.handleGetItemGroups(args); break; case 'sap_create_business_partner': result = await this.masterDataHandler.handleCreateBusinessPartner(args); break; case 'sap_update_business_partner': result = await this.masterDataHandler.handleUpdateBusinessPartner(args); break; case 'sap_get_items': result = await this.masterDataHandler.handleGetItems(args); break; case 'sap_get_item': result = await this.masterDataHandler.handleGetItem(args); break; case 'sap_create_item': result = await this.masterDataHandler.handleCreateItem(args); break; case 'sap_get_warehouses': result = await this.masterDataHandler.handleGetWarehouses(args); break; case 'sap_get_warehouse': result = await this.masterDataHandler.handleGetWarehouse(args); break; case 'sap_get_pricelists': result = await this.masterDataHandler.handleGetPriceLists(args); break; case 'sap_get_pricelist': result = await this.masterDataHandler.handleGetPriceList(args); break; case 'sap_get_item_stock': result = await this.masterDataHandler.handleGetItemStock(args); break; case 'sap_get_item_stock_all': result = await this.masterDataHandler.handleGetItemStockAll(args); break; case 'sap_get_item_price': result = await this.masterDataHandler.handleGetItemPrice(args); break; case 'sap_get_item_prices_all': result = await this.masterDataHandler.handleGetItemPricesAll(args); break; // Document tools case 'sap_get_orders': result = await this.documentHandler.handleGetOrders(args); break; case 'sap_get_order': result = await this.documentHandler.handleGetOrder(args); break; case 'sap_create_order': result = await this.documentHandler.handleCreateOrder(args); break; case 'sap_get_purchase_orders': result = await this.documentHandler.handleGetPurchaseOrders(args); break; case 'sap_get_purchase_order': result = await this.documentHandler.handleGetPurchaseOrder(args); break; case 'sap_create_purchase_order': result = await this.documentHandler.handleCreatePurchaseOrder(args); break; case 'sap_get_goods_receipts': result = await this.documentHandler.handleGetGoodsReceipts(args); break; case 'sap_get_goods_receipt': result = await this.documentHandler.handleGetGoodsReceipt(args); break; case 'sap_get_invoices': result = await this.documentHandler.handleGetInvoices(args); break; case 'sap_get_invoice': result = await this.documentHandler.handleGetInvoice(args); break; case 'sap_create_invoice': result = await this.documentHandler.handleCreateInvoice(args); break; // Query tools case 'sap_query': result = await this.queryHandler.handleQuery(args); break; case 'sap_cross_join': result = await this.queryHandler.handleCrossJoin(args); break; default: throw new Error(`Unknown tool: ${name}`); } const duration = Date.now() - startTime; logger.logToolExecution(name, args, result.success !== false, duration); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error) { const duration = Date.now() - startTime; logger.error('Tool execution failed', error, { tool: name }); logger.logToolExecution(name, args, false, duration); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error), tool: name }, null, 2) } ] }; } }); } /** * Get server information */ getServerInfo() { return { name: 'sap-b1-mcp-server', version: '1.0.0', description: 'SAP Business One Service Layer MCP Server', config: this.configManager?.getConfigSummary(), toolsCount: authTools.length + masterDataTools.length + documentTools.length + queryTools.length }; } } /** * Main entry point */ async function main() { const server = new SAPMCPServer(); // Handle graceful shutdown const shutdown = async (signal) => { logger.info(`Received ${signal}. Shutting down gracefully...`); try { await server.stop(); process.exit(0); } catch (error) { logger.error('Error during shutdown', error); process.exit(1); } }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error('Uncaught exception', error); shutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled promise rejection', new Error(String(reason)), { promise }); shutdown('unhandledRejection'); }); try { // Get config file path from command line argument const configPath = process.argv[2]; await server.initialize(configPath); await server.start(); } catch (error) { logger.error('Failed to start server', error); process.exit(1); } } // Run if this is the main module if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); } // SAPMCPServer is already exported as a class above //# sourceMappingURL=index.js.map