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