UNPKG

vela-mcp

Version:

FastMCP Server wrapper for Vela.MCP - MCP-compatible interface to the Vela.MCP REST API

1,202 lines • 51.5 kB
#!/usr/bin/env node /** * FastMCP Server wrapper for Vela.MCP * * This provides an MCP-compatible interface to the Vela.MCP REST API, * using the MCP SDK which is the recommended approach for Claude Desktop. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // ============================================================================ // Configuration // ============================================================================ // Configuration from environment variables (overridable via CLI args) let API_BASE_URL = process.env.VELA_API_URL || "http://localhost:8000"; let API_KEY = process.env.VELA_API_KEY || ""; let PROJECT_ID = process.env.VELA_PROJECT_ID || ""; /** * Parse command line arguments for configuration overrides */ function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === "--api-url" && i + 1 < argv.length) { out.apiUrl = argv[++i]; } else if (arg === "--api-key" && i + 1 < argv.length) { out.apiKey = argv[++i]; } else if (arg === "--project-id" && i + 1 < argv.length) { out.projectId = argv[++i]; } } return out; } // Parse and apply command line arguments const argOverrides = parseArgs(process.argv.slice(2)); if (argOverrides.apiUrl) API_BASE_URL = argOverrides.apiUrl; if (argOverrides.apiKey) API_KEY = argOverrides.apiKey; if (argOverrides.projectId) PROJECT_ID = argOverrides.projectId; // ============================================================================ // Logging and HTTP Client Configuration // ============================================================================ /** * Log to stderr (required for MCP protocol) */ const log = (...args) => { console.error(...args); }; /** * Get HTTP headers for API requests */ const getHeaders = () => { const headers = { "Content-Type": "application/json", }; if (API_KEY) { headers["X-API-Key"] = API_KEY; } if (PROJECT_ID) { headers["X-Project-ID"] = PROJECT_ID; } return headers; }; // ============================================================================ // API Client // ============================================================================ /** * Call the Vela API and return the response * @param endpoint - API endpoint name (e.g., "TableFieldAnalyzer") or path (e.g., "/ask") * @param data - Query data including filters and parameters * @returns API response data * @throws Error if request fails or returns non-OK status */ async function callVelaAPI(endpoint, data) { try { const headers = getHeaders(); let url; let method; let body; if (endpoint.startsWith("/")) { // Direct endpoint url = `${API_BASE_URL}${endpoint}`; if (data) { method = "POST"; body = JSON.stringify(data); } else { method = "GET"; } } else { // Tool query url = `${API_BASE_URL}/query`; method = "POST"; const queryData = { tool_id: endpoint, version_target: data?.version_target || "source", params: data?.params || {}, filters: data?.filters || {}, }; log("DEBUG: Sending API request:", JSON.stringify(queryData)); body = JSON.stringify(queryData); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30_000); try { const response = await fetch(url, { method, headers, body, signal: controller.signal, }); if (!response.ok) { const errorText = await response.text(); log(`HTTP error ${response.status}: ${errorText}`); throw new Error(`API error: ${response.status}`); } return await response.json(); } finally { clearTimeout(timeoutId); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Request error: ${errorMessage}`); throw new Error(`Request failed: ${errorMessage}`); } } // ============================================================================ // Validation Functions // ============================================================================ /** * Validate and normalize object ID * @param objectId - Object ID to validate (must be numeric) * @returns Normalized object ID string * @throws Error if object ID is invalid */ function validateObjectId(objectId) { if (objectId === null || objectId === undefined) { throw new Error("object_id is required"); } const idStr = String(objectId).trim(); if (!/^\d+$/.test(idStr)) { throw new Error(`object_id must be a number (e.g., '50099' or 50099), got: ${idStr}`); } return idStr; } /** * Validate and normalize object type * @param objectType - Object type to validate * @returns Normalized object type (lowercase) * @throws Error if object type is invalid */ function validateObjectType(objectType) { const validTypes = [ "Codeunit", "Table", "Page", "Report", "Query", "XMLPort", "Enum", "TableExtension", "PageExtension", ]; if (!objectType) { throw new Error("object_type is required"); } const normalized = objectType.trim().toLowerCase(); for (const validType of validTypes) { if (normalized === validType.toLowerCase()) { return validType.toLowerCase(); } } throw new Error(`object_type must be one of: ${validTypes.join(", ")}. Got: ${objectType}`); } /** * Validate version target parameter * @param versionTarget - Version target to validate ("source" or "target") * @returns Normalized version target (lowercase) * @throws Error if version target is invalid */ function validateVersionTarget(versionTarget) { const validTargets = ["source", "target"]; if (!validTargets.includes(versionTarget.toLowerCase())) { throw new Error(`version_target must be 'source' or 'target', got: ${versionTarget}`); } return versionTarget.toLowerCase(); } /** * Validate severity level * @param severity - Severity level (1-5) * @returns Validated severity as number * @throws Error if severity is invalid */ function validateSeverity(severity) { try { const sevInt = parseInt(String(severity), 10); if (sevInt < 1 || sevInt > 5) { throw new Error(`severity must be between 1 and 5, got: ${sevInt}`); } return sevInt; } catch { throw new Error(`severity must be a number between 1-5, got: ${severity}`); } } // ============================================================================ // Tool Implementation Functions // ============================================================================ /** * Identify deprecated patterns and risky upgrade tags in Business Central code * @param args - Tool arguments including filters and parameters * @returns Analysis of deprecated code patterns */ async function deprecated_code_explorer(args) { try { const { version_target = "source", include_details = false, scan_dependencies = false, object_type = "Codeunit", object_id, severity = 1, } = args || {}; const validatedVersionTarget = validateVersionTarget(String(version_target)); const validatedObjectType = validateObjectType(String(object_type)); const validatedSeverity = validateSeverity(severity); const filters = { object_type: validatedObjectType, severity: validatedSeverity }; if (object_id) filters.object_id = validateObjectId(object_id); const data = { version_target: validatedVersionTarget, filters, params: { include_details: Boolean(include_details), scan_dependencies: Boolean(scan_dependencies), }, }; const result = await callVelaAPI("DeprecatedCodeExplorer", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Find SaaS-incompatible code patterns for migration planning * @param args - Tool arguments including filters and parameters * @returns Analysis of SaaS incompatibility issues */ async function saas_incompatibility_explorer(args) { try { const { version_target = "source", include_solutions = true, pattern_type, severity = 1, object_type, object_id, } = args || {}; const filters = { severity: Number(severity) }; if (pattern_type) filters.pattern_type = String(pattern_type); if (object_type) filters.object_type = validateObjectType(String(object_type)); if (object_id) filters.object_id = validateObjectId(object_id); const data = { version_target: validateVersionTarget(String(version_target)), filters, params: { include_solutions: Boolean(include_solutions) }, }; const result = await callVelaAPI("SAASIncompatibilityExplorer", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Analyze table structure and relationships between entities * @param args - Tool arguments including table filters and relationship depth * @returns Table structure and relationship analysis */ async function data_model_rebuilder(args) { try { const { version_target = "source", include_fields = true, include_relations = false, relation_depth = 1, table_name, table_id, } = args || {}; const data = { version_target: validateVersionTarget(String(version_target)), params: { include_fields: Boolean(include_fields), include_relations: Boolean(include_relations), relation_depth: Number(relation_depth), }, filters: {}, }; if (table_name) data.filters.table_name = String(table_name); if (table_id) data.filters.table_id = validateObjectId(table_id); const result = await callVelaAPI("DataModelRebuilder", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Analyze table structure with complete field definitions, data types, and properties * Always includes full details: field definitions, relations, options, validation, and extensions * @param args - Tool arguments (table_name or table_id required) * @returns Complete table field analysis */ async function table_field_analyzer(args) { try { const { table_name, table_id, version_target = "source" } = args || {}; if (!table_name && !table_id) { return { result: "Parameter Error: Either table_name or table_id is required\n\nExamples:\n table_field_analyzer(table_name='Customer')\n table_field_analyzer(table_id='18')", }; } let validatedTableId; if (table_id) validatedTableId = validateObjectId(table_id); const data = { version_target: validateVersionTarget(String(version_target)), params: { include_field_details: true, include_relations: true, include_options: true, include_validation: true, include_extensions: true, // CRITICAL FIX: Added missing parameter }, filters: {}, }; if (table_name) data.filters.table_name = String(table_name).trim(); if (validatedTableId) data.filters.table_id = validatedTableId; const result = await callVelaAPI("TableFieldAnalyzer", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Find extensions (TableExtension, PageExtension, etc.) for a base object * @param args - Tool arguments including base object type and optional filters * @returns List of extensions for the specified base object */ async function extension_finder(args) { try { const { base_object_type, base_object_id, base_object_name, extension_type, version_target = "source", include_field_details = true, include_action_details = false, include_procedure_details = false, } = args || {}; const validatedVersionTarget = validateVersionTarget(String(version_target)); const validatedBaseObjectType = validateObjectType(String(base_object_type)); const filters = { base_object_type: validatedBaseObjectType }; if (base_object_id) filters.base_object_id = validateObjectId(base_object_id); if (base_object_name && String(base_object_name).trim()) { filters.base_object_name = String(base_object_name).trim(); } if (extension_type && String(extension_type).trim()) { filters.extension_type = String(extension_type).trim(); } const data = { version_target: validatedVersionTarget, filters, params: { include_field_details: Boolean(include_field_details), include_action_details: Boolean(include_action_details), include_procedure_details: Boolean(include_procedure_details), }, }; const result = await callVelaAPI("ExtensionFinder", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Search and resolve Business Central objects by name instead of ID * Supports fuzzy matching for finding objects with partial names * @param args - Tool arguments including search term and optional filters * @returns List of matching objects with IDs and similarity scores */ async function object_name_resolver(args) { try { const { search_term, object_type, fuzzy_match = true, max_results = 20, min_score = 0.3, version_target = "source", } = args || {}; if (!search_term || !String(search_term).trim()) { return { result: "Parameter Error: search_term is required. Please provide an object name to search for." }; } const validatedVersionTarget = validateVersionTarget(String(version_target)); const data = { version_target: validatedVersionTarget, filters: { search_term: String(search_term).trim() }, params: { fuzzy_match: Boolean(fuzzy_match), max_results: Math.max(1, Math.min(100, Number(max_results))), min_score: Math.max(0.0, Math.min(1.0, Number(min_score))), }, }; if (object_type) { const validTypes = [ "table", "codeunit", "page", "report", "query", "xmlport", "enum", "tableextension", "pageextension", "reportextension", ]; if (!validTypes.includes(String(object_type).toLowerCase())) { return { result: `Parameter Error: object_type must be one of: ${validTypes.join(", ")}. Got: ${object_type}` }; } data.filters.object_type = String(object_type).toLowerCase(); } const result = await callVelaAPI("ObjectNameResolver", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * List all procedures in a Business Central object * @param args - Tool arguments including object type and ID * @returns List of procedures with signatures and metadata */ async function procedure_lister(args) { try { const { object_type, object_id, version_target = "source", include_signatures = true, include_local = true, procedure_type, } = args || {}; const filters = { object_type: validateObjectType(String(object_type)), object_id: validateObjectId(object_id), }; if (procedure_type) filters.procedure_type = String(procedure_type); const data = { version_target: validateVersionTarget(String(version_target)), filters, params: { include_signatures: Boolean(include_signatures), include_local: Boolean(include_local), }, }; const result = await callVelaAPI("ProcedureLister", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Comprehensive upgrade analysis combining multiple tools * @param args - Tool arguments including object type and ID * @returns Consolidated upgrade impact report */ async function consolidated_upgrade_report(args) { try { const { object_type, object_id, object_name, version_target = "source", include_data = true, include_details = true, scan_dependencies = false, } = args || {}; // Validate that either object_id or object_name is provided if (!object_id && !object_name) { return { result: "Parameter Error: Either object_id or object_name is required\n\nExamples:\n consolidated_upgrade_report(object_type='Codeunit', object_id='50003')\n consolidated_upgrade_report(object_type='Table', object_name='Customer')" }; } const filters = { object_type: validateObjectType(String(object_type)), }; if (object_id) { filters.object_id = validateObjectId(object_id); } if (object_name && String(object_name).trim()) { filters.object_name = String(object_name).trim(); } const data = { version_target: validateVersionTarget(String(version_target)), filters, params: { include_data: Boolean(include_data), include_details: Boolean(include_details), scan_dependencies: Boolean(scan_dependencies), }, }; const result = await callVelaAPI("ConsolidatedUpgradeReport", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Find upgrade/obsolete tags for objects and fields * @param args - Tool arguments including object and optional field identifiers * @returns Upgrade tag information */ async function upgrade_tag_explorer(args) { try { const { object_type, object_id, object_name, field_id, field_name, version_target = "target", } = args || {}; const filters = { object_type: validateObjectType(String(object_type)) }; if (object_id) filters.object_id = validateObjectId(object_id); if (object_name) filters.object_name = String(object_name).trim(); if (field_id) filters.field_id = validateObjectId(field_id); if (field_name) filters.field_name = String(field_name).trim(); if (!object_id && !object_name) { return { result: "Parameter Error: Either object_id or object_name is required" }; } const data = { version_target: validateVersionTarget(String(version_target)), filters, params: {}, }; const result = await callVelaAPI("UpgradeTagExplorer", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * List event publishers for a specific object or wildcard pattern * @param args - Tool arguments including object type/ID or wildcard search * @returns List of event publishers */ async function event_publisher_explorer(args) { try { const { object_type, object_id, wildcard, version_target = "source" } = args || {}; const filters = {}; if (object_type) filters.object_type = validateObjectType(String(object_type)); if (object_id) filters.object_id = validateObjectId(object_id); if (wildcard) filters.wildcard = String(wildcard).trim(); if (!Object.keys(filters).length) { return { result: "Parameter Error: Provide either object_type/object_id or wildcard" }; } const data = { version_target: validateVersionTarget(String(version_target)), filters, params: {}, }; const result = await callVelaAPI("EventPublisherExplorer", data); return { result: formatToolResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } // ============================================================================ // Response Formatting Functions // ============================================================================ /** * Format API response for general tool outputs * @param result - API response data * @returns Formatted string response */ function formatToolResponse(result) { if (typeof result === "object" && result !== null) { const res = result; if ("error" in res) { return `Error: ${res.error}`; } const contentParts = []; if ("summary" in res) { contentParts.push(`**Summary**: ${res.summary}`); } const dataSections = []; if (Array.isArray(res.results)) { dataSections.push(["Results", res.results]); } if (Array.isArray(res.web_services)) { dataSections.push(["Web Services", res.web_services]); } if (Array.isArray(res.api_endpoints)) { dataSections.push(["API Endpoints", res.api_endpoints]); } if (Array.isArray(res.nodes)) { dataSections.push(["Event Nodes", res.nodes]); } if (Array.isArray(res.edges)) { dataSections.push(["Event Connections", res.edges]); } if (res.diff && typeof res.diff === "object") { const diff = res.diff; for (const changeType of ["added", "removed", "modified", "unchanged"]) { if (Array.isArray(diff[changeType]) && diff[changeType].length) { dataSections.push([`${String(changeType).charAt(0).toUpperCase()}${String(changeType).slice(1)} Items`, diff[changeType]]); } } } if (Array.isArray(res.findings)) { dataSections.push(["Upgrade Impact Findings", res.findings]); } if (!dataSections.length) { for (const [key, value] of Object.entries(res)) { if (["error", "summary", "metadata", "statistics", "analysis"].includes(key)) continue; if (Array.isArray(value) && value.length) { dataSections.push([key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()), value]); } else if (value && typeof value === "object") { for (const [subkey, subvalue] of Object.entries(value)) { if (Array.isArray(subvalue) && subvalue.length) { dataSections.push([ `${key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())} - ${subkey.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}`, subvalue, ]); } } } } } for (const [sectionName, dataList] of dataSections) { if (!dataList || dataList.length === 0) { contentParts.push(`\n**${sectionName}**: No items found`); continue; } contentParts.push(`\n**${sectionName}** (${dataList.length} items):`); for (let i = 0; i < Math.min(10, dataList.length); i++) { const item = dataList[i]; let itemStr; if (item && typeof item === "object") { try { itemStr = JSON.stringify(item, null, 2); } catch (e) { const error = e; itemStr = `[Serialization Error: ${String(error?.message || e)}] ${String(item)}`; } } else { itemStr = String(item); } contentParts.push(`\n${i + 1}. ${itemStr}`); } if (dataList.length > 10) { contentParts.push(`\n... and ${dataList.length - 10} more items`); } } if (res.metadata && typeof res.metadata === "object") { const metadata = res.metadata; contentParts.push(`\n**Metadata**:`); contentParts.push(`- Analysis Mode: ${metadata.analysis_mode ?? "Unknown"}`); if (metadata.filters_applied) { const filters = metadata.filters_applied; try { if (Object.values(filters).some((v) => Boolean(v))) { contentParts.push(`- Filters Applied: ${JSON.stringify(filters)}`); } } catch { // Ignore serialization errors } } } return contentParts.length ? contentParts.join("\n") : JSON.stringify(result, null, 2); } return String(result); } /** * Format event flow analysis response with upgrade impact details * @param result - Event flow API response * @returns Formatted event flow analysis */ function formatEventFlowResponse(result) { if (typeof result !== "object" || result === null) return String(result); const res = result; if ("error" in res) return `Error: ${res.error}`; const parts = []; if (res.upgrade_impact_summary) { const summary = res.upgrade_impact_summary; parts.push("🚨 **UPGRADE IMPACT ANALYSIS**"); parts.push(`**Breaking Subscriptions**: ${summary.unique_breaking_subscriptions ?? 0}`); parts.push(`**Affected Procedures**: ${summary.total_affected_procedures ?? 0}`); parts.push(`**Recommendation**: ${summary.recommendation ?? "No issues found"}`); if (Array.isArray(summary.breaking_subscriptions) && summary.breaking_subscriptions.length) { parts.push("\n**Breaking Event Subscriptions:**"); for (let i = 0; i < Math.min(10, summary.breaking_subscriptions.length); i++) { const breakingSub = summary.breaking_subscriptions[i]; if (breakingSub && typeof breakingSub === "object") { const eventName = breakingSub.event_name ?? "Unknown Event"; const publisherObj = breakingSub.expected_publisher_object ?? "Unknown Object"; const subscriberCount = breakingSub.subscriber_count ?? 0; parts.push(`${i + 1}. **${eventName}** (Expected in ${publisherObj}) - ${subscriberCount} subscribers affected`); } else { parts.push(`${i + 1}. ${String(breakingSub)}`); } } if (summary.breaking_subscriptions.length > 10) { parts.push(`... and ${summary.breaking_subscriptions.length - 10} more breaking subscriptions`); } } } if (res.summary) parts.push(`\n**Analysis Summary**: ${res.summary}`); if (res.metadata && typeof res.metadata === "object") { parts.push("\n**Metadata**:"); for (const [k, v] of Object.entries(res.metadata)) { parts.push(`- ${k.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())}: ${String(v)}`); } } return parts.length ? parts.join("\n") : JSON.stringify(result, null, 2); } /** * Format dependency analysis response with breaking dependencies highlighted * @param result - Dependency analysis API response * @returns Formatted dependency analysis */ function formatDependencyResponse(result) { if (typeof result !== "object" || result === null) return String(result); const res = result; if ("error" in res) return `Error: ${res.error}`; const parts = []; if (res.upgrade_impact_summary) { const summary = res.upgrade_impact_summary; parts.push("🚨 **DEPENDENCY UPGRADE IMPACT ANALYSIS**"); parts.push(`**Total Dependencies Checked**: ${summary.total_dependencies_checked ?? 0}`); parts.push(`**Compatible**: ${summary.compatible ?? 0}`); parts.push(`**Breaking**: ${summary.breaking ?? 0}`); parts.push(`**Custom Objects Affected**: ${summary.custom_objects_affected ?? 0}`); parts.push(`**Recommendation**: ${summary.recommendation ?? "No issues found"}`); if (summary.breaking_by_type && typeof summary.breaking_by_type === "object") { parts.push("\n**Breaking Dependencies by Type:**"); for (const [depType, count] of Object.entries(summary.breaking_by_type)) { parts.push(`- ${depType}: ${count}`); } } } if (Array.isArray(res.breaking_dependencies) && res.breaking_dependencies.length) { const breakingDeps = res.breaking_dependencies; parts.push(`\n**Breaking Dependencies** (${breakingDeps.length} found):`); for (let i = 0; i < Math.min(10, breakingDeps.length); i++) { const dep = breakingDeps[i]; if (dep && typeof dep === "object") { const sourceObj = dep.source_object || {}; const dependency = dep.dependency || {}; const severity = dep.severity || "unknown"; const isCustom = Boolean(dep.is_custom); const customMarker = isCustom ? " [CUSTOM]" : ""; const objInfo = `${sourceObj.object_type || "Unknown"} ${sourceObj.object_id || "?"}`; let depInfo = dependency.target_table || "Unknown"; if (dependency.target_field) depInfo += `.${dependency.target_field}`; parts.push(`${i + 1}. [${String(severity).toUpperCase()}]${customMarker} ${objInfo} → ${depInfo}`); parts.push(` Reason: ${dep.break_reason || "Unknown"}`); parts.push(` Recommendation: ${dep.recommendation || "Review manually"}`); } else { parts.push(`${i + 1}. ${String(dep)}`); } } if (breakingDeps.length > 10) { parts.push(`\n... and ${breakingDeps.length - 10} more breaking dependencies`); } } if (Array.isArray(res.compatible_dependencies)) { const compatCount = res.compatible_dependencies.length; if (compatCount > 0) { parts.push(`\nāœ… **${compatCount} Compatible Dependencies** (exist in TARGET)`); } } // Return formatted output if we have content, otherwise return full JSON return parts.length ? parts.join("\n") : JSON.stringify(result, null, 2); } /** * Ask natural language questions about the codebase using AI-powered search * @param args - Tool arguments including query and search parameters * @returns AI-generated answer with citations and provenance */ async function ask(args) { try { const { query, version_target = "source", strict_mode = false, provenance = true, temperature = 0.0, max_tools = 3, } = args; if (!query || !String(query).trim()) { return { result: "Parameter Error: query cannot be empty. Please provide a question about the codebase.", }; } const validatedVersionTarget = validateVersionTarget(String(version_target)); const payload = { query: String(query).trim(), version_target: validatedVersionTarget, strict_mode: Boolean(strict_mode), provenance: Boolean(provenance), temperature: Number(temperature), max_tools: Math.max(1, Number(max_tools)), }; const result = await callVelaAPI("/ask", payload); if (typeof result === "object" && result !== null) { const response = { result: result.summary || "No summary available", }; if ("citations" in result && result.citations) { response.citations = result.citations; } if ("tools_used" in result) { response.tools_used = result.tools_used; } if ("grounded" in result) { response.grounded = result.grounded; } return response; } else { return { result: formatToolResponse(result) }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Validate table and field dependencies across versions to identify breaking changes * @param args - Tool arguments including object filters and dependency types * @returns Dependency analysis with breaking changes highlighted */ async function dependency_explorer(args) { try { const { object_type, object_id, version_target = "source", dependency_types } = args || {}; const validatedVersionTarget = validateVersionTarget(String(version_target)); const filters = {}; if (object_type) { filters.object_type = validateObjectType(String(object_type)); } if (object_id) { filters.object_id = validateObjectId(object_id); } const params = {}; if (dependency_types) params.dependency_types = dependency_types; const data = { version_target: validatedVersionTarget, filters, params, }; const result = await callVelaAPI("DependencyExplorer", data); return { result: formatDependencyResponse(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } /** * Find breaking event subscriptions for specified objects * @param args - Tool arguments including object filters and event name * @returns Event flow analysis with subscription impacts */ async function event_flow_tracker(args) { try { const { object_type, object_id, event_name, version_target = "source", include_data = true } = args || {}; const validatedVersionTarget = validateVersionTarget(String(version_target)); const filters = {}; if (object_type) filters.object_type = validateObjectType(String(object_type)); if (object_id) filters.object_id = validateObjectId(object_id); if (event_name) filters.event_name = String(event_name); const data = { version_target: validatedVersionTarget, filters, params: { include_data: Boolean(include_data) }, }; const result = await callVelaAPI("EventFlowTracker", data); if (result && typeof result === "object" && "results" in result && Array.isArray(result.results) && result.results.length > 0 && result.results[0] && typeof result.results[0] === "object" && "value" in result.results[0]) { return { result: String(result.results[0].value) }; } return { result: String(result) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { result: `Error: ${errorMessage}` }; } } // ============================================================================ // Tool Definitions (MCP SDK) // ============================================================================ /** * Array of all available tools with their schemas */ const TOOLS = [ { name: "ask", description: "Ask natural language questions about your Business Central codebase using AI-powered search", inputSchema: { type: "object", properties: { query: { type: "string", description: "Natural language question about the codebase", }, version_target: { type: "string", enum: ["source", "target"], default: "source", description: "Which codebase version to search", }, strict_mode: { type: "boolean", default: false, }, provenance: { type: "boolean", default: true, }, temperature: { type: "number", default: 0.0, }, max_tools: { type: "number", default: 3, }, }, required: ["query"], }, }, { name: "dependency_explorer", description: "Validate table and field dependencies across versions to identify breaking changes", inputSchema: { type: "object", properties: { object_type: { type: "string", description: "Object type to analyze" }, object_id: { type: "string", description: "Numeric ID of the object" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, dependency_types: { type: "array", items: { type: "string" } }, }, }, }, { name: "event_flow_tracker", description: "Find breaking event subscriptions for specified objects", inputSchema: { type: "object", properties: { object_type: { type: "string" }, object_id: { type: "string" }, event_name: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_data: { type: "boolean", default: true }, }, }, }, { name: "deprecated_code_explorer", description: "Identify deprecated patterns and risky upgrade tags", inputSchema: { type: "object", properties: { version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_details: { type: "boolean", default: false }, scan_dependencies: { type: "boolean", default: false }, object_type: { type: "string", default: "Codeunit" }, object_id: { type: "string" }, severity: { type: "number", default: 1 }, }, }, }, { name: "saas_incompatibility_explorer", description: "Find SaaS-incompatible code patterns for migration planning", inputSchema: { type: "object", properties: { version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_solutions: { type: "boolean", default: true }, pattern_type: { type: "string" }, severity: { type: "number", default: 1 }, object_type: { type: "string" }, object_id: { type: "string" }, }, }, }, { name: "data_model_rebuilder", description: "Analyze table structure and relationships between entities", inputSchema: { type: "object", properties: { version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_fields: { type: "boolean", default: true }, include_relations: { type: "boolean", default: false }, relation_depth: { type: "number", default: 1 }, table_name: { type: "string" }, table_id: { type: "string" }, }, }, }, { name: "table_field_analyzer", description: "Analyze table structure with complete field definitions, data types, and properties", inputSchema: { type: "object", properties: { table_name: { type: "string" }, table_id: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, }, }, }, { name: "extension_finder", description: "Find extensions (TableExtension, PageExtension, etc.) for a base object", inputSchema: { type: "object", properties: { base_object_type: { type: "string" }, base_object_id: { type: "string" }, base_object_name: { type: "string" }, extension_type: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_field_details: { type: "boolean", default: true }, include_action_details: { type: "boolean", default: false }, include_procedure_details: { type: "boolean", default: false }, }, required: ["base_object_type"], }, }, { name: "object_name_resolver", description: "Search and resolve Business Central objects by name instead of ID", inputSchema: { type: "object", properties: { search_term: { type: "string" }, object_type: { type: "string" }, fuzzy_match: { type: "boolean", default: true }, max_results: { type: "number", default: 20 }, min_score: { type: "number", default: 0.3 }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, }, required: ["search_term"], }, }, { name: "procedure_lister", description: "List all procedures in a Business Central object", inputSchema: { type: "object", properties: { object_type: { type: "string" }, object_id: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, include_signatures: { type: "boolean", default: true }, include_local: { type: "boolean", default: true }, procedure_type: { type: "string" }, }, required: ["object_type", "object_id"], }, }, { name: "consolidated_upgrade_report", description: "Comprehensive upgrade analysis combining multiple tools (event subscriptions, dependencies, and SaaS incompatibilities). Requires object_type, version_target, and either object_id or object_name.", inputSchema: { type: "object", properties: { object_type: { type: "string", description: "Type of object (e.g., 'Codeunit', 'Table', 'Page') - REQUIRED" }, object_id: { type: "string", description: "Numeric ID of the object (e.g., '50099') - Either object_id or object_name is REQUIRED" }, object_name: { type: "string", description: "Name of the object - Either object_id or object_name is REQUIRED" }, version_target: { type: "string", enum: ["source", "target"], default: "source", description: "Which codebase version to analyze - REQUIRED" }, include_data: { type: "boolean", default: true, description: "Include detailed data in the response" }, include_details: { type: "boolean", default: true, description: "Include detailed analysis" }, scan_dependencies: { type: "boolean", default: false, description: "Scan and analyze dependencies" }, }, required: ["object_type", "version_target"], }, }, { name: "upgrade_tag_explorer", description: "Find upgrade/obsolete tags for objects and fields", inputSchema: { type: "object", properties: { object_type: { type: "string" }, object_id: { type: "string" }, object_name: { type: "string" }, field_id: { type: "string" }, field_name: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "target" }, }, required: ["object_type"], }, }, { name: "event_publisher_explorer", description: "List event publishers for a specific object or wildcard", inputSchema: { type: "object", properties: { object_type: { type: "string" }, object_id: { type: "string" }, wildcard: { type: "string" }, version_target: { type: "string", enum: ["source", "target"], default: "source" }, }, }, }, ]; // ============================================================================ // MCP Server Setup and Main Entry Point // ============================================================================ /** * Main function to initialize and run the MCP server */ async function main() { const server = new Server({ name: "vela-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS, }; }); // Call tool handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const toolArgs = (args || {}); try { let result; switch (name) { case "ask": result = await ask(toolArgs); break; case "dependency_explorer": result = await dependency_explorer(toolArgs); break; case "event_flow_tracker": result = await event_flow_tracker(toolArgs); break; case "deprecated_code_explorer": result = await deprecated_code_explorer(toolArgs); break; case "saas_incompatibility_explorer": result = await saas_incompatibility_explorer(toolArgs); break; case "data_model_rebuilder": result = await data_model_rebuilder(toolArgs); break; case "table_field_analyzer": result = await table_field_analyzer(toolArgs); break; case "extension_finder": result = await extension_finder(toolArgs); break; case "object_name_resolver": result = await object_name_resolver(toolArgs); break; case "procedure_lister": result = await procedure_lister(toolArgs); break; case "consolidated_upgrade_report": result = await consolidated_upgrade_report(toolArgs); break; case "upgrade_tag_explorer": result = await upgrade_tag_explorer(toolArgs); break; case "event_publisher_explorer": result = await event_publisher_explorer(toolArgs); break; default: thro