vela-mcp
Version:
FastMCP Server wrapper for Vela.MCP - MCP-compatible interface to the Vela.MCP REST API
1,473 lines (1,344 loc) ⢠49.9 kB
text/typescript
/**
* 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,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================================================
// Type Definitions
// ============================================================================
/**
* Result returned by tool execution functions
*/
interface ToolResult {
result?: string;
citations?: Record<string, unknown>[];
tools_used?: Record<string, unknown>[];
grounded?: boolean;
}
/**
* Command line argument overrides
*/
interface ArgOverrides {
apiUrl?: string;
apiKey?: string;
projectId?: string;
}
/**
* Filter object for API queries
*/
interface QueryFilters {
object_type?: string;
object_id?: string;
table_name?: string;
table_id?: string;
base_object_type?: string;
base_object_id?: string;
base_object_name?: string;
extension_type?: string;
search_term?: string;
severity?: number;
pattern_type?: string;
wildcard?: string;
event_name?: string;
field_id?: string;
field_name?: string;
object_name?: string;
procedure_type?: string;
}
/**
* Parameters object for API queries
*/
interface QueryParams {
include_details?: boolean;
scan_dependencies?: boolean;
include_solutions?: boolean;
include_fields?: boolean;
include_field_details?: boolean;
include_relations?: boolean;
include_options?: boolean;
include_validation?: boolean;
include_extensions?: boolean;
include_action_details?: boolean;
include_procedure_details?: boolean;
include_signatures?: boolean;
include_local?: boolean;
include_data?: boolean;
dependency_types?: string[];
relation_depth?: number;
fuzzy_match?: boolean;
max_results?: number;
min_score?: number;
}
/**
* API query data structure
*/
interface QueryData {
version_target: string;
params?: QueryParams;
filters?: QueryFilters;
include_fields?: boolean;
include_relations?: boolean;
relation_depth?: number;
}
// ============================================================================
// 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: string[]): ArgOverrides {
const out: ArgOverrides = {};
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: unknown[]): void => {
console.error(...args);
};
/**
* Get HTTP headers for API requests
*/
const getHeaders = (): Record<string, string> => {
const headers: Record<string, string> = {
"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: string, data?: QueryData | Record<string, unknown>): Promise<unknown> {
try {
const headers = getHeaders();
let url: string;
let method: string;
let body: string | undefined;
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 as QueryData)?.version_target || "source",
params: (data as QueryData)?.params || {},
filters: (data as QueryData)?.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: unknown): string {
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: string): string {
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: string): string {
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: unknown): number {
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: Record<string, unknown>): Promise<ToolResult> {
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: QueryFilters = {
object_type: validatedObjectType,
severity: validatedSeverity
};
if (object_id) filters.object_id = validateObjectId(object_id);
const data: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const {
version_target = "source",
include_solutions = true,
pattern_type,
severity = 1,
object_type,
object_id,
} = args || {};
const filters: QueryFilters = { 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: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const {
version_target = "source",
include_fields = true,
include_relations = false,
relation_depth = 1,
table_name,
table_id,
} = args || {};
const data: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
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: string | undefined;
if (table_id) validatedTableId = validateObjectId(table_id);
const data: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
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: QueryFilters = { 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: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
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: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const {
object_type,
object_id,
version_target = "source",
include_signatures = true,
include_local = true,
procedure_type,
} = args || {};
const filters: QueryFilters = {
object_type: validateObjectType(String(object_type)),
object_id: validateObjectId(object_id),
};
if (procedure_type) filters.procedure_type = String(procedure_type);
const data: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
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: QueryFilters = {
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: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const {
object_type,
object_id,
object_name,
field_id,
field_name,
version_target = "target",
} = args || {};
const filters: QueryFilters = {
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: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const { object_type, object_id, wildcard, version_target = "source" } = args || {};
const filters: QueryFilters = {};
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: QueryData = {
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: unknown): string {
if (typeof result === "object" && result !== null) {
const res = result as Record<string, unknown>;
if ("error" in res) {
return `Error: ${res.error}`;
}
const contentParts: string[] = [];
if ("summary" in res) {
contentParts.push(`**Summary**: ${res.summary}`);
}
const dataSections: Array<[string, unknown[]]> = [];
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 as Record<string, unknown>;
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 as Record<string, unknown>)) {
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: string;
if (item && typeof item === "object") {
try {
itemStr = JSON.stringify(item, null, 2);
} catch (e) {
const error = e as Error;
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 as Record<string, unknown>;
contentParts.push(`\n**Metadata**:`);
contentParts.push(`- Analysis Mode: ${metadata.analysis_mode ?? "Unknown"}`);
if (metadata.filters_applied) {
const filters = metadata.filters_applied as Record<string, unknown>;
try {
if (Object.values(filters).some((v: unknown) => 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: unknown): string {
if (typeof result !== "object" || result === null) return String(result);
const res = result as Record<string, unknown>;
if ("error" in res) return `Error: ${res.error}`;
const parts: string[] = [];
if (res.upgrade_impact_summary) {
const summary = res.upgrade_impact_summary as Record<string, unknown>;
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 as Record<string, unknown>)) {
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: unknown): string {
if (typeof result !== "object" || result === null) return String(result);
const res = result as Record<string, unknown>;
if ("error" in res) return `Error: ${res.error}`;
const parts: string[] = [];
if (res.upgrade_impact_summary) {
const summary = res.upgrade_impact_summary as Record<string, unknown>;
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: any = 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: Record<string, unknown>): Promise<ToolResult> {
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: ToolResult = {
result: (result as Record<string, unknown>).summary as string || "No summary available",
};
if ("citations" in result && result.citations) {
response.citations = result.citations as Record<string, unknown>[];
}
if ("tools_used" in result) {
response.tools_used = result.tools_used as Record<string, unknown>[];
}
if ("grounded" in result) {
response.grounded = result.grounded as boolean;
}
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: Record<string, unknown>): Promise<ToolResult> {
try {
const { object_type, object_id, version_target = "source", dependency_types } = args || {};
const validatedVersionTarget = validateVersionTarget(String(version_target));
const filters: QueryFilters = {};
if (object_type) {
filters.object_type = validateObjectType(String(object_type));
}
if (object_id) {
filters.object_id = validateObjectId(object_id);
}
const params: QueryParams = {};
if (dependency_types) params.dependency_types = dependency_types as string[];
const data: QueryData = {
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: Record<string, unknown>): Promise<ToolResult> {
try {
const { object_type, object_id, event_name, version_target = "source", include_data = true } = args || {};
const validatedVersionTarget = validateVersionTarget(String(version_target));
const filters: QueryFilters = {};
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: QueryData = {
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: Tool[] = [
{
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 || {}) as Record<string, unknown>;
try {
let result: ToolResult;
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:
throw new Error(`Unknown tool: ${name}`);
}
const parts: string[] = [];
if (result.result) parts.push(result.result);
if (result.citations) parts.push(`\nCitations:\n${JSON.stringify(result.citations, null, 2)}`);
if (result.tools_used) parts.push(`\nTools Used:\n${JSON.stringify(result.tools_used, null, 2)}`);
if (typeof result.grounded === "boolean") parts.push(`\nGrounded: ${result.grounded ? "Yes" : "No"}`);
return {
content: [
{
type: "text",
text: parts.join("\n"),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error executing tool: ${error.message}`,
},
],
isError: true,
};
}
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
log("Starting Vela FastMCP server...");
log(`API URL: ${API_BASE_URL}`);
log(`API Key configured: ${API_KEY ? "Yes" : "No"}`);
log(`Project ID configured: ${PROJECT_ID ? "Yes" : "No"}`);
}
// Run the server
main().catch((error) => {
console.error("Fatal error in MCP server:", error);
process.exit(1);
});