UNPKG

inecta-food-knowledge-mcp

Version:

MCP server for INECTA Food Knowledge Base - semantic search and dependency analysis for Business Central AL code and documentation

616 lines (602 loc) 29.8 kB
#!/usr/bin/env node /** * inecta Food Knowledge Base MCP Server * * Provides 13 tools for semantic search and dependency analysis * of Business Central AL code, documentation, and inecta University user manuals. * * Tool Groups: * 1. Search & Content (4 tools): search_knowledge_base, find_similar_logic, find_similar_code, get_document_content * 2. Discovery (1 tool): list_modules * 3. Object Registry (4 tools): get_object_info, get_next_available_id, list_reserved_ids, release_reserved_id * 4. Dependency Analysis (2 tools): get_dependencies, generate_call_graph * 5. Advanced Analysis (2 tools): map_event_chain, analyze_table_usage */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; dotenv.config(); // API configuration - env vars override obfuscated defaults // Legacy fallbacks kept for existing users during migration const getConfig = () => { const url = process.env.API_URL || Buffer.from('aHR0cHM6Ly9jbG91ZC1hcGktbmpuNTNheGVvYS11Yy5hLnJ1bi5hcHA=', 'base64').toString('utf-8'); const token = process.env.API_TOKEN || Buffer.from('aW5lY3RhX2ludGVybmFsX2Rldl90b2tlbl8yMDI1', 'base64').toString('utf-8'); return { url, token }; }; const config = getConfig(); /** * Helper function to call Backend API */ async function callAPI(endpoint, method = 'GET', body) { const url = `${config.url}${endpoint}`; const options = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.token}`, }, }; if (body && method !== 'GET') { options.body = JSON.stringify(body); } const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`API error (${response.status}): ${errorText}`); } return await response.json(); } /** * MCP Server setup */ const server = new Server({ name: 'inecta-food-knowledge', version: '0.4.2', description: 'inecta Food Knowledge Base - Semantic search for Business Central documentation and code. IMPORTANT: Always write company name as lowercase "inecta" (not INECTA or Inecta) per brand guidelines.', }, { capabilities: { tools: {}, }, }); /** * Tool definitions - organized by category */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // ============================================ // GROUP 1: Search & Content (4 tools) // ============================================ { name: 'search_knowledge_base', description: `Search inecta Food Knowledge Base using hybrid semantic search. Returns document metadata and previews (snippets). For full content, use get_document_content tool. PARAMETER USAGE RULES: 1. General search → omit content_types 2. User mentions "video guide", "training video", "inecta University", "workflow guide", or "step-by-step" → set content_types: ["university_article"] 3. User explicitly asks for code → set content_types: ["al_code"] 4. User explicitly asks for technical docs → set content_types: ["l1_documentation"] 5. User explicitly asks for user guides → set content_types: ["l2_documentation"] Example: Query "Find inecta University video guide for X" → content_types: ["university_article"]`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language search query (e.g., "how to post production orders")', }, audience: { type: 'string', enum: ['developer', 'end_user', 'functional_user'], description: 'Target audience for results filtering', default: 'functional_user', }, content_types: { type: 'array', items: { type: 'string', enum: ['l1_documentation', 'l2_documentation', 'al_code', 'university_article'] }, description: 'Filter by content type. Use ["university_article"] for video guides, training videos, inecta University content, and workflow guides. Use ["al_code"] for AL code. Use ["l1_documentation"] for technical reference. Use ["l2_documentation"] for user manuals. Omit for general search across all types.', }, modules: { type: 'array', items: { type: 'string' }, description: 'Optional: Filter by specific modules (e.g., ["YPRODO", "YMAINT"])', }, top_k: { type: 'number', description: 'Number of results to return (default: 5, max: 20)', default: 5, }, }, required: ['query'], }, }, { name: 'find_similar_logic', description: `Find AL objects with similar business logic using semantic search on L1 documentation. Use this when you want to find objects that do similar things conceptually (e.g., "posting routines", "validation logic"). Accepts ONE of these inputs: - query: Find objects matching a natural language description - code_snippet: Find objects similar to a piece of AL code - object_id + object_type: Find objects similar to a known AL object (BOTH REQUIRED together) REQUIRED: When using object_id, you MUST also provide object_type. The API will reject requests with object_id but no object_type because the same ID can exist across different object types (e.g., table 37010138 vs codeunit 37010138). Returns L1 documentation for similar objects with relevance scores.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language description of the logic to find (e.g., "production order posting")', }, code_snippet: { type: 'string', description: 'AL code snippet to find similar logic for', }, object_id: { type: 'string', description: 'Object ID to find similar logic for - REQUIRES object_type to be specified', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'tableextension', 'pageextension', 'enum', 'query'], description: 'AL object type - REQUIRED when using object_id. Same ID exists across types (e.g., table 37010138 vs codeunit 37010138)', }, module: { type: 'string', description: 'Filter by module (e.g., "YPRODO")', }, top_k: { type: 'number', description: 'Number of results to return (default: 5, max: 20)', default: 5, }, }, required: [], }, }, { name: 'find_similar_code', description: `Find AL code with similar structure or implementation. Use this when you want to find code with similar syntax or structure (e.g., "procedures with same signature", "similar event subscribers"). Accepts ONE of these inputs: - code_snippet: AL code to find similar code for - object_id + object_type: Find code similar to a known AL object (BOTH REQUIRED together) - object_id + object_type + component_name: Find code similar to a specific procedure/trigger REQUIRED: When using object_id, you MUST also provide object_type. The API will reject requests with object_id but no object_type because the same ID can exist across different object types (e.g., table 37010138 vs codeunit 37010138). Returns AL code chunks with similarity scores and code previews. Note: Requires al_code to be indexed.`, inputSchema: { type: 'object', properties: { code_snippet: { type: 'string', description: 'AL code snippet to find similar code for', }, object_id: { type: 'string', description: 'Object ID to find similar code for - REQUIRES object_type to be specified', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'tableextension', 'pageextension', 'enum', 'query'], description: 'AL object type - REQUIRED when using object_id. Same ID exists across types (e.g., table 37010138 vs codeunit 37010138)', }, component_name: { type: 'string', description: 'Specific procedure/trigger name when using object_id + object_type', }, module: { type: 'string', description: 'Filter results by module (e.g., "YPRODO")', }, include_tests: { type: 'boolean', description: 'Include test codeunits in results (default: false)', default: false, }, top_k: { type: 'number', description: 'Number of results to return (default: 5, max: 20)', default: 5, }, }, required: [], }, }, { name: 'get_document_content', description: 'Fetch full content for a specific document by ID. Returns complete document text from storage. Use after search_knowledge_base to get full content for selected documents.', inputSchema: { type: 'object', properties: { document_id: { type: 'string', description: 'Document ID from search results (e.g., "l1_YPRODO_codeunits_37010169_ProductionPostingManager")', }, }, required: ['document_id'], }, }, // ============================================ // GROUP 2: Discovery (1 tool) // ============================================ { name: 'list_modules', description: 'List all available Business Central modules in the knowledge base', inputSchema: { type: 'object', properties: {}, }, }, // ============================================ // GROUP 3: Object Registry (4 tools) // ============================================ { name: 'get_object_info', description: `Unified AL object lookup. Accepts object ID (37M range or 50K range) OR object name, and returns full object information including both ID formats, module, and file path. Detects conflicts when same ID exists in different repos. Examples: - get_object_info("37009705", "codeunit") → lookup by 37M ID - get_object_info("59705", "codeunit") → lookup by 50K ID (same object) - get_object_info("CaseManagementFunctionsINE", "codeunit") → lookup by name`, inputSchema: { type: 'object', properties: { identifier: { type: 'string', description: 'Object ID (37M or 50K range) OR object name', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'tableextension', 'pageextension', 'enum', 'query'], description: 'AL object type', }, module: { type: 'string', description: 'Optional: Filter by module name (useful to disambiguate conflicts)', }, }, required: ['identifier', 'object_type'], }, }, { name: 'get_next_available_id', description: 'Get the next available object ID for a module. Useful when creating new objects. Automatically creates a soft reservation (expires after 3 weeks) to prevent ID conflicts across branches.', inputSchema: { type: 'object', properties: { module: { type: 'string', description: 'Module name (e.g., "YCASEM", "YPRODO")', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'tableextension', 'pageextension', 'enum', 'query'], description: 'AL object type', }, branch_name: { type: 'string', description: 'Git branch name where the object will be created (e.g., "feature/new-posting-routine")', }, }, required: ['module', 'object_type', 'branch_name'], }, }, { name: 'list_reserved_ids', description: 'List all active (non-expired) soft ID reservations. Useful for seeing which IDs are reserved by other developers.', inputSchema: { type: 'object', properties: { module: { type: 'string', description: 'Filter by module name (optional)', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'tableextension', 'pageextension', 'enum', 'query'], description: 'Filter by object type (optional)', }, reserved_by: { type: 'string', description: 'Filter by user email (optional)', }, }, required: [], }, }, { name: 'release_reserved_id', description: 'Release a soft ID reservation early. Use when a branch is abandoned or the ID is no longer needed. IMPORTANT: Only release ONE reservation at a time - wait for confirmation before releasing another.', inputSchema: { type: 'object', properties: { reservation_id: { type: 'number', description: 'The reservation ID to release (from list_reserved_ids)', }, }, required: ['reservation_id'], }, }, // ============================================ // GROUP 4: Dependency Analysis (2 tools) // ============================================ { name: 'get_dependencies', description: `Analyze object dependencies with multi-level traversal. Shows relationships: calls, reads, writes, deletes, subscribes, extends. Use cases: - Impact analysis: "What would break if I change this table?" - Understanding code flow: "What does this codeunit call?" - Finding consumers: "What objects read from this table?" Returns grouped results by level and relationship type with Mermaid diagram.`, inputSchema: { type: 'object', properties: { object_id: { type: 'string', description: 'Object ID (e.g., "37010147" or "37010232")', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report', 'query', 'enum', 'tableextension', 'pageextension'], description: 'AL object type', }, direction: { type: 'string', enum: ['uses', 'used_by', 'both'], description: 'Direction: "uses" (what this calls), "used_by" (what calls this), or "both" (default)', default: 'both', }, depth: { type: ['number', 'string'], description: 'Traversal depth: 1-20 for specific depth, or "all" for unlimited (default: 1)', default: 1, }, max_results: { type: 'number', description: 'Maximum results to return (default: 500, max: 2000)', default: 500, }, }, required: ['object_id', 'object_type'], }, }, { name: 'generate_call_graph', description: 'Generate a Mermaid call graph diagram showing procedure call relationships. Uses recursive traversal to show multi-level call chains.', inputSchema: { type: 'object', properties: { object_id: { type: 'string', description: 'Object ID', }, object_type: { type: 'string', enum: ['codeunit', 'table', 'page', 'report'], description: 'Object type', }, direction: { type: 'string', enum: ['forward', 'backward', 'both'], description: 'Direction: "forward" (what it calls), "backward" (what calls it), "both" (default)', default: 'both', }, max_depth: { type: 'number', description: 'Maximum call depth (default: 2, max: 10)', default: 2, }, }, required: ['object_id', 'object_type'], }, }, // ============================================ // GROUP 5: Advanced Analysis (2 tools) // ============================================ { name: 'map_event_chain', description: 'Map event publisher/subscriber chains. Shows who publishes an event and who subscribes to it. Useful for understanding event-driven code flow.', inputSchema: { type: 'object', properties: { event_name: { type: 'string', description: 'Event name to search for (partial match supported)', }, publisher_module: { type: 'string', description: 'Optional: Filter by publisher module', }, publisher_object_id: { type: 'string', description: 'Optional: Filter by publisher object ID', }, }, required: ['event_name'], }, }, { name: 'analyze_table_usage', description: 'Analyze which objects read, insert, modify, or delete from a table. Useful for understanding data flow and impact of table changes.', inputSchema: { type: 'object', properties: { table_name: { type: 'string', description: 'Table name (partial match supported)', }, table_id: { type: 'string', description: 'Table ID (exact match)', }, operation_types: { type: 'array', items: { type: 'string', enum: ['read', 'insert', 'modify', 'delete'] }, description: 'Optional: Filter by operation types', }, limit: { type: 'number', description: 'Maximum results to return (default: 100)', default: 100, }, }, }, }, ], }; }); /** * Tool execution handler */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // GROUP 1: Search & Content case 'search_knowledge_base': { const result = await callAPI('/api/search', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'find_similar_logic': { // Validate: object_type is REQUIRED when object_id is provided const { object_id, object_type, code_snippet, query } = args; if (object_id && !object_type) { throw new Error('object_type is REQUIRED when using object_id. The same object_id can exist across different object types (e.g., table 37010138 vs codeunit 37010138). Please specify object_type: "codeunit", "table", "page", "report", etc.'); } if (!object_id && !code_snippet && !query) { throw new Error('At least one input required: query, code_snippet, or object_id (with object_type)'); } const result = await callAPI('/api/similar-logic', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'find_similar_code': { // Validate: object_type is REQUIRED when object_id is provided const { object_id, object_type, code_snippet } = args; if (object_id && !object_type) { throw new Error('object_type is REQUIRED when using object_id. The same object_id can exist across different object types (e.g., table 37010138 vs codeunit 37010138). Please specify object_type: "codeunit", "table", "page", "report", etc.'); } if (!object_id && !code_snippet) { throw new Error('At least one input required: code_snippet or object_id (with object_type)'); } const result = await callAPI('/api/similar-code', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_document_content': { const document_id = args?.document_id; if (!document_id) { throw new Error('document_id is required'); } const result = await callAPI(`/api/documents/${encodeURIComponent(document_id)}`, 'GET'); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } // GROUP 2: Discovery case 'list_modules': { const result = await callAPI('/api/modules', 'GET'); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } // GROUP 3: Object Registry case 'get_object_info': { const result = await callAPI('/api/object/info', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'get_next_available_id': { const { module, object_type, branch_name } = args; const url = `/api/object/next-id/${encodeURIComponent(module)}/${encodeURIComponent(object_type)}?branch_name=${encodeURIComponent(branch_name)}`; const result = await callAPI(url, 'GET'); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'list_reserved_ids': { const { module, object_type, reserved_by } = args; const params = new URLSearchParams(); if (module) params.append('module', module); if (object_type) params.append('object_type', object_type); if (reserved_by) params.append('reserved_by', reserved_by); const queryString = params.toString(); const url = `/api/object/reservations${queryString ? '?' + queryString : ''}`; const result = await callAPI(url, 'GET'); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'release_reserved_id': { const { reservation_id } = args; const result = await callAPI(`/api/object/reservations/${reservation_id}`, 'DELETE'); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } // GROUP 4: Dependency Analysis case 'get_dependencies': { const result = await callAPI('/api/dependencies', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'generate_call_graph': { const result = await callAPI('/api/call-graph', 'POST', args); return { content: [{ type: 'text', text: result.mermaid_diagram || JSON.stringify(result, null, 2) }], }; } // GROUP 5: Advanced Analysis case 'map_event_chain': { const result = await callAPI('/api/event-chain', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'analyze_table_usage': { const result = await callAPI('/api/table-usage', 'POST', args); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); /** * Start the MCP server */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('inecta Food Knowledge Base MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); //# sourceMappingURL=index.js.map