sap-b1-mcp-server
Version:
SAP Business One Service Layer MCP Server
408 lines • 17.5 kB
JavaScript
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