UNPKG

sap-b1-mcp-server

Version:

SAP Business One Service Layer MCP Server

531 lines 19.8 kB
import { z } from 'zod'; // Schema for sap_query tool const querySchema = z.object({ entityType: z.enum([ 'BusinessPartners', 'Items', 'Orders', 'Invoices', 'DeliveryNotes', 'PurchaseOrders', 'PurchaseInvoices', 'Quotations', 'CreditNotes', 'DebitNotes', 'CorrectionInvoice', 'DownPayments', 'DownPaymentsToDraw', 'PurchaseDownPayments', 'Warehouses', 'PriceLists' ]).describe('SAP B1 entity type to query'), filter: z.string().optional().describe('OData $filter expression'), select: z.string().optional().describe('OData $select expression to specify fields'), expand: z.string().optional().describe('OData $expand expression to include related data'), orderby: z.string().optional().describe('OData $orderby expression'), top: z.number().optional().describe('Maximum number of records to return'), skip: z.number().optional().describe('Number of records to skip for pagination'), count: z.boolean().optional().describe('Include count of total records') }); // Schema for sap_cross_join tool const crossJoinSchema = z.object({ entities: z.array(z.string()).describe('List of entity types to join (e.g., ["BusinessPartners", "Orders"])'), filter: z.string().optional().describe('Cross-entity filter expression'), select: z.string().optional().describe('Fields to select from joined entities'), orderby: z.string().optional().describe('Order by expression for joined results'), top: z.number().optional().describe('Maximum number of records to return') }); export const queryTools = [ { name: 'sap_query', description: 'Execute custom OData queries against any SAP Business One entity with full OData support including filtering, selection, expansion, ordering, and pagination.', inputSchema: { type: 'object', properties: { entityType: { type: 'string', enum: [ 'BusinessPartners', 'Items', 'Orders', 'Invoices', 'DeliveryNotes', 'PurchaseOrders', 'PurchaseInvoices', 'Quotations', 'CreditNotes', 'DebitNotes', 'CorrectionInvoice', 'DownPayments', 'DownPaymentsToDraw', 'PurchaseDownPayments' ], description: 'SAP B1 entity type to query' }, filter: { type: 'string', description: 'OData $filter expression (e.g., "CardCode eq \'C20000\' and DocDate ge \'2024-01-01\'")' }, select: { type: 'string', description: 'OData $select expression to specify fields (e.g., "DocEntry,DocNum,CardCode,CardName")' }, expand: { type: 'string', description: 'OData $expand expression to include related data (e.g., "DocumentLines,BusinessPartner")' }, orderby: { type: 'string', description: 'OData $orderby expression (e.g., "DocDate desc,DocTotal asc")' }, top: { type: 'number', description: 'Maximum number of records to return' }, skip: { type: 'number', description: 'Number of records to skip for pagination' }, count: { type: 'boolean', description: 'Include count of total records' } }, required: ['entityType'], additionalProperties: false } }, { name: 'sap_cross_join', description: 'Execute cross-join queries for complex data relationships across multiple SAP Business One entities.', inputSchema: { type: 'object', properties: { entities: { type: 'array', items: { type: 'string' }, description: 'List of entity types to join (e.g., ["BusinessPartners", "Orders"])' }, filter: { type: 'string', description: 'Cross-entity filter expression' }, select: { type: 'string', description: 'Fields to select from joined entities' }, orderby: { type: 'string', description: 'Order by expression for joined results' }, top: { type: 'number', description: 'Maximum number of records to return' } }, required: ['entities'], additionalProperties: false } } ]; export class QueryToolHandler { sapClient; constructor(sapClient) { this.sapClient = sapClient; } async handleQuery(args) { try { const params = querySchema.parse(args); // Build query parameters const queryParams = {}; if (params.filter) queryParams.$filter = params.filter; if (params.select) queryParams.$select = params.select; if (params.expand) queryParams.$expand = params.expand; if (params.orderby) queryParams.$orderby = params.orderby; if (params.top) queryParams.$top = params.top; if (params.skip) queryParams.$skip = params.skip; if (params.count) queryParams.$count = params.count; const results = await this.sapClient.executeQuery(params.entityType, queryParams); // Prepare response with metadata const response = { success: true, entityType: params.entityType, count: results.length, data: results }; // Add query summary for debugging const queryOperations = []; if (params.filter) queryOperations.push('filtered'); if (params.select) queryOperations.push('selected fields'); if (params.expand) queryOperations.push('expanded relations'); if (params.orderby) queryOperations.push('ordered'); if (params.top || params.skip) queryOperations.push('paginated'); if (queryOperations.length > 0) { response.queryOperations = queryOperations; } return response; } catch (error) { return { success: false, error: error.message || 'Query execution failed', details: 'Check your OData syntax and ensure the session is valid', entityType: args?.entityType || 'unknown' }; } } async handleCrossJoin(args) { try { const params = crossJoinSchema.parse(args); // Validate entities if (params.entities.length < 2) { throw new Error('Cross-join requires at least 2 entities'); } // Build cross-join query string const joinQuery = params.entities.join(','); let queryString = joinQuery; // Add query parameters const queryParts = []; if (params.filter) queryParts.push(`$filter=${encodeURIComponent(params.filter)}`); if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`); if (params.orderby) queryParts.push(`$orderby=${encodeURIComponent(params.orderby)}`); if (params.top) queryParts.push(`$top=${params.top}`); if (queryParts.length > 0) { queryString += '?' + queryParts.join('&'); } const results = await this.sapClient.executeCrossJoinQuery(queryString); return { success: true, entities: params.entities, count: results.length, data: results, joinType: 'cross-join' }; } catch (error) { return { success: false, error: error.message || 'Cross-join query execution failed', details: 'Check entity names and join syntax', entities: args?.entities || [] }; } } /** * Helper method to build and execute advanced queries with validation */ async executeAdvancedQuery(entityType, options) { try { // Validate filter syntax if provided if (options.filter && !this.validateODataFilter(options.filter)) { throw new Error('Invalid OData filter syntax'); } // Validate select fields if provided if (options.select && !this.validateODataSelect(options.select)) { throw new Error('Invalid OData select syntax'); } // Execute query const results = await this.sapClient.executeQuery(entityType, options); return { success: true, entityType, queryOptions: options, count: results.length, data: results }; } catch (error) { return { success: false, entityType, error: error.message || 'Advanced query execution failed' }; } } /** * Helper method to get query suggestions based on entity type */ getQuerySuggestions(entityType) { const suggestions = { BusinessPartners: { commonFilters: [ "CardType eq 'cCustomer'", "CardType eq 'cSupplier'", "Valid eq 'tYES'", "CardName contains 'Microsoft'" ], commonSelects: [ "CardCode,CardName,CardType", "CardCode,CardName,EmailAddress,Phone1", "CardCode,CardName,Address,ZipCode" ], commonOrderBy: [ "CardName asc", "CreationDate desc", "CardCode asc" ] }, Items: { commonFilters: [ "ItemType eq 'it_Item'", "SalesItem eq 'tYES'", "PurchaseItem eq 'tYES'", "QuantityOnStock gt 0" ], commonSelects: [ "ItemCode,ItemName,QuantityOnStock", "ItemCode,ItemName,SalesItem,PurchaseItem", "ItemCode,ItemName,BarCode" ], commonOrderBy: [ "ItemName asc", "QuantityOnStock desc", "ItemCode asc" ] }, Orders: { commonFilters: [ "DocDate ge '2024-01-01'", "DocStatus eq 'bost_Open'", "CardCode eq 'C20000'", "DocTotal gt 1000" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName,DocTotal", "DocEntry,DocNum,DocDate,DocDueDate,DocTotal", "DocEntry,DocNum,CardCode,DocStatus" ], commonOrderBy: [ "DocDate desc", "DocTotal desc", "DocNum desc" ], commonExpands: [ "DocumentLines", "BusinessPartner", "DocumentLines,BusinessPartner" ] }, Invoices: { commonFilters: [ "DocDate ge '2024-01-01'", "CardCode eq 'C20000'", "DocTotal gt 1000", "Paid eq 'tNO'" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName,DocTotal", "DocEntry,DocNum,DocDate,DocDueDate,DocTotal", "DocEntry,DocNum,CardCode,VatSum" ], commonOrderBy: [ "DocDate desc", "DocTotal desc", "DocNum desc" ], commonExpands: [ "DocumentLines", "BusinessPartner", "DocumentLines,BusinessPartner" ] }, DeliveryNotes: { commonFilters: [ "DocDate ge '2024-01-01'", "CardCode eq 'C20000'" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName", "DocEntry,DocNum,DocDate,DocTotal" ], commonOrderBy: [ "DocDate desc", "DocNum desc" ] }, PurchaseOrders: { commonFilters: [ "DocDate ge '2024-01-01'", "DocStatus eq 'bost_Open'", "CardCode eq 'V20000'" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName,DocTotal", "DocEntry,DocNum,DocDate,DocDueDate" ], commonOrderBy: [ "DocDate desc", "DocTotal desc" ] }, PurchaseInvoices: { commonFilters: [ "DocDate ge '2024-01-01'", "CardCode eq 'V20000'" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName,DocTotal", "DocEntry,DocNum,DocDate,VatSum" ], commonOrderBy: [ "DocDate desc", "DocTotal desc" ] }, Quotations: { commonFilters: [ "DocDate ge '2024-01-01'", "DocStatus eq 'bost_Open'" ], commonSelects: [ "DocEntry,DocNum,CardCode,CardName,DocTotal", "DocEntry,DocNum,DocDate,DocDueDate" ], commonOrderBy: [ "DocDate desc" ] }, CreditNotes: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, DebitNotes: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, CorrectionInvoice: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, DownPayments: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, DownPaymentsToDraw: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, PurchaseDownPayments: { commonFilters: [ "DocDate ge '2024-01-01'" ], commonSelects: [ "DocEntry,DocNum,CardCode,DocTotal" ], commonOrderBy: [ "DocDate desc" ] }, Warehouses: { commonFilters: [ "Inactive eq 'tNO'", "WarehouseName contains 'Main'" ], commonSelects: [ "WarehouseCode,WarehouseName,Inactive", "WarehouseCode,WarehouseName,Country,State,City", "WarehouseCode,WarehouseName,Address,ZipCode,Phone1" ], commonOrderBy: [ "WarehouseName asc", "WarehouseCode asc" ] }, PriceLists: { commonFilters: [ "Active eq 'tYES'", "PriceListName contains 'Standard'" ], commonSelects: [ "PriceListNo,PriceListName,Active", "PriceListNo,PriceListName,IsGrossPrice,Factor", "PriceListNo,PriceListName,ValidFrom,ValidTo" ], commonOrderBy: [ "PriceListName asc", "PriceListNo asc" ] } }; return suggestions[entityType] || { message: 'No specific suggestions available for this entity type' }; } validateODataFilter(filter) { // Basic validation for OData filter syntax const validOperators = ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or', 'not', 'contains', 'startswith', 'endswith']; const lowercaseFilter = filter.toLowerCase(); // Check for balanced parentheses let parenthesesCount = 0; for (const char of filter) { if (char === '(') parenthesesCount++; if (char === ')') parenthesesCount--; if (parenthesesCount < 0) return false; } if (parenthesesCount !== 0) return false; // Check if filter contains at least one valid operator return validOperators.some(op => lowercaseFilter.includes(op)); } validateODataSelect(select) { // Basic validation for OData select syntax // Should be comma-separated field names, optionally with navigation properties const selectPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\([^)]+\))?(\/[a-zA-Z_][a-zA-Z0-9_]*(\([^)]+\))?)*(\s*,\s*[a-zA-Z_][a-zA-Z0-9_]*(\([^)]+\))?(\/[a-zA-Z_][a-zA-Z0-9_]*(\([^)]+\))?)*)*$/; return selectPattern.test(select.trim()); } } //# sourceMappingURL=queries.js.map