UNPKG

bc-webclient-mcp

Version:

Model Context Protocol (MCP) server for Microsoft Dynamics 365 Business Central via WebUI protocol. Enables AI assistants to interact with BC through the web client protocol, supporting Card, List, and Document pages with full line item support and server

178 lines 7.76 kB
/** * Real BC Integration Test Server * * Starts MCP server with REAL BC connection for end-to-end testing. * Communicates via JSON-RPC 2.0 over stdio. * * Usage: * npm run test:mcp:real * or * tsx src/test-mcp-server-real.ts */ import { MCPServer, StdioTransport } from './services/index.js'; import { GetPageMetadataTool, SearchPagesTool, ReadPageDataTool, WritePageDataTool, ExecuteActionTool, HandleDialogTool, StartWorkflowTool, GetWorkflowStateTool, EndWorkflowTool, // Consolidated from 9 tools to 6 core tools // FilterListTool removed - merged into read_page_data.filters // FindRecordTool removed - thin wrapper, users compose directly // CreateRecordTool, UpdateRecordTool - moved to optional (not in default registry) // UpdateFieldTool removed - merged into write_page_data } from './tools/index.js'; // Use BCPageConnection for connection-per-page architecture (fixes BC caching issue) import { BCPageConnection } from './connection/bc-page-connection.js'; import { isOk } from './core/result.js'; import { bcConfig } from './core/config.js'; import { AuditLogger } from './services/audit-logger.js'; import { BCConnectionPool } from './services/connection-pool.js'; import { CacheManager } from './services/cache-manager.js'; // Console logger implementation class ConsoleLogger { debug(message, context) { const contextStr = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[DEBUG] ${message}${contextStr}`); } info(message, context) { const contextStr = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[INFO] ${message}${contextStr}`); } warn(message, context) { const contextStr = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[WARN] ${message}${contextStr}`); } error(message, error, context) { const errorStr = error ? ` ${String(error)}` : ''; const contextStr = context ? ` ${JSON.stringify(context)}` : ''; console.error(`[ERROR] ${message}${errorStr}${contextStr}`); } child(context) { return this; } } async function main() { const logger = new ConsoleLogger(); // Get BC connection config from centralized config const { baseUrl, username, password, tenantId } = bcConfig; if (!password) { logger.error('BC_PASSWORD environment variable not set'); process.exit(1); } const connection = new BCPageConnection({ baseUrl, username, password, tenantId, timeout: 30000, }); const connectResult = await connection.connect(); if (!isOk(connectResult)) { logger.error(`Failed to connect to BC: ${connectResult.error.message}`); process.exit(1); } // Create connection pool for Tell Me searches logger.info('Initializing connection pool...'); const connectionPool = new BCConnectionPool({ baseUrl }, username, password, tenantId, { minConnections: 1, // Reduced to 1 to avoid BC rate limiting maxConnections: 10, idleTimeoutMs: 300000, // 5 minutes healthCheckIntervalMs: 60000, // 1 minute acquireTimeoutMs: 30000, // 30 seconds }); await connectionPool.initialize(); logger.info(`Connection pool initialized (${connectionPool.getStats().available} connections ready)`); // Create cache manager logger.info('Initializing cache manager...'); const cacheManager = new CacheManager({ maxEntries: 1000, defaultTtlMs: 300000, // 5 minutes for searches cleanupIntervalMs: 60000, // 1 minute enableCoalescing: true, }); logger.info('Cache manager initialized'); // Create MCP server const server = new MCPServer(logger); // Create audit logger for tracking consent-required tool executions const auditLogger = new AuditLogger(logger, 10000); // Keep last 10k events // Register 6 Core MCP Tools (MCP best practice: 4-6 tools for context efficiency) // // Consolidated from 9 tools to reduce context pollution and improve composability. // See Refactor1.md for analysis and rationale. // Read-only tools (no audit logger needed) server.registerTool(new GetPageMetadataTool(connection, bcConfig)); server.registerTool(new SearchPagesTool(bcConfig, connectionPool, cacheManager)); server.registerTool(new ReadPageDataTool(connection, bcConfig)); // Now includes filtering // Write/mutation tools (with audit logger for consent tracking) server.registerTool(new WritePageDataTool(connection, bcConfig, auditLogger)); server.registerTool(new ExecuteActionTool(connection, bcConfig, auditLogger)); server.registerTool(new HandleDialogTool(connection, bcConfig, auditLogger)); // Workflow state management tools (no BC connection needed - work with WorkflowStateManager singleton) server.registerTool(new StartWorkflowTool()); server.registerTool(new GetWorkflowStateTool()); server.registerTool(new EndWorkflowTool()); // Removed from default registry: // - FilterListTool: Functionality available via read_page_data.filters parameter // - FindRecordTool: Users compose with read_page_data + filters directly // - CreateRecordTool: Moved to optional/ (users can compose: get_page_metadata → execute_action("New") → write_page_data) // - UpdateRecordTool: Moved to optional/ (users can compose: get_page_metadata → execute_action("Edit") → write_page_data) // Initialize server const initResult = await server.initialize(); if (!isOk(initResult)) { logger.error(`Failed to initialize server: ${initResult.error.message}`); cacheManager.shutdown(); await connectionPool.shutdown(); await connection.close(); process.exit(1); } // Create stdio transport const transport = new StdioTransport(server, { logger, enableDebugLogging: false, // Set to true for verbose logging }); const transportResult = await transport.start(); if (!isOk(transportResult)) { logger.error(`Failed to start transport: ${transportResult.error.message}`); cacheManager.shutdown(); await connectionPool.shutdown(); await connection.close(); process.exit(1); } // Start server const startResult = await server.start(); if (!isOk(startResult)) { logger.error(`Failed to start server: ${startResult.error.message}`); cacheManager.shutdown(); await connectionPool.shutdown(); await connection.close(); process.exit(1); } logger.info('BC MCP Server ready'); // Output ready signal to stdout for test client detection // This allows tests to start immediately instead of waiting a fixed timeout console.log('__MCP_SERVER_READY__'); // Handle graceful shutdown process.on('SIGINT', async () => { logger.info('Received SIGINT, shutting down...'); await transport.stop(); await server.stop(); cacheManager.shutdown(); await connectionPool.shutdown(); await connection.close(); logger.info('Shutdown complete'); process.exit(0); }); process.on('SIGTERM', async () => { logger.info('Received SIGTERM, shutting down...'); await transport.stop(); await server.stop(); cacheManager.shutdown(); await connectionPool.shutdown(); await connection.close(); logger.info('Shutdown complete'); process.exit(0); }); // Server now runs until stdin closes or process exits } // Run main main().catch(async (error) => { console.error('[ERROR] Fatal error', error); process.exit(1); }); //# sourceMappingURL=test-mcp-server-real.js.map