Skip to main content
Glama
index.ts11.1 kB
/** * Trace MCP - Schema Comparator * Compares producer schemas (backend) against consumer usage (frontend) */ import type { ProducerSchema, ConsumerSchema, TraceResult, Mismatch, Match, SourceLocation } from '../types.js'; import type { DataFlowDirection } from '../core/types.js'; export interface CompareOptions { /** Strict mode: treat missing optional properties as warnings */ strict?: boolean; /** Ignore certain property names */ ignoreProperties?: string[]; /** * Data flow direction for compatibility checking * - producer_to_consumer: API response flow (extra producer fields OK) * - consumer_to_producer: API request flow (extra consumer fields OK) * - bidirectional: Must match exactly * @default "producer_to_consumer" */ direction?: DataFlowDirection; } /** * Compare backend producer schemas against frontend consumer expectations */ export function compareSchemas( producers: ProducerSchema[], consumers: ConsumerSchema[], options: CompareOptions = {} ): TraceResult { // Default to producer_to_consumer (API response pattern) const direction = options.direction || 'producer_to_consumer'; console.log(`[Comparator] Comparing ${producers.length} producers vs ${consumers.length} consumers`); console.log(`[Comparator] Direction: ${direction}`); const matches: Match[] = []; const mismatches: Mismatch[] = []; // Index producers by tool name for quick lookup const producerMap = new Map<string, ProducerSchema>(); for (const producer of producers) { producerMap.set(producer.toolName, producer); } // Analyze each consumer usage for (const consumer of consumers) { const producer = producerMap.get(consumer.toolName); if (!producer) { // Tool not found in producer definitions mismatches.push({ toolName: consumer.toolName, issueType: 'UNKNOWN_TOOL', description: `Tool "${consumer.toolName}" is called but not defined in producer`, consumerLocation: consumer.callSite, }); continue; } // Check argument mismatches (consumer → producer direction) const argMismatches = checkArgumentMismatches(producer, consumer, direction); mismatches.push(...argMismatches); // Check expected property mismatches (producer → consumer direction) const propMismatches = checkPropertyMismatches(producer, consumer, options, direction); mismatches.push(...propMismatches); // If no mismatches for this call, it's a match if (argMismatches.length === 0 && propMismatches.length === 0) { matches.push({ toolName: consumer.toolName, producerLocation: producer.location, consumerLocation: consumer.callSite, }); } } return { timestamp: new Date().toISOString(), producerSource: producers.length > 0 ? producers[0].location.file : '', consumerSource: consumers.length > 0 ? consumers[0].callSite.file : '', matches, mismatches, summary: { totalTools: producers.length, totalCalls: consumers.length, matchCount: matches.length, mismatchCount: mismatches.length, }, }; } /** * Check if consumer provides correct arguments to the tool * This represents consumer → producer data flow (request direction) */ function checkArgumentMismatches( producer: ProducerSchema, consumer: ConsumerSchema, direction: DataFlowDirection ): Mismatch[] { const mismatches: Mismatch[] = []; const inputSchema = producer.inputSchema; if (!inputSchema.properties) { return mismatches; } const requiredArgs = inputSchema.required || []; const definedArgs = Object.keys(inputSchema.properties); const providedArgs = Object.keys(consumer.argumentsProvided); // Check for missing required arguments // This is ALWAYS an error regardless of direction for (const required of requiredArgs) { if (!providedArgs.includes(required)) { mismatches.push({ toolName: consumer.toolName, issueType: 'ARGUMENT_ERROR', description: `Missing required argument "${required}"`, producerLocation: producer.location, consumerLocation: consumer.callSite, details: { missingArg: required, providedArgs }, }); } } // Check for unknown arguments (consumer → producer direction) // Treatment depends on direction: // - consumer_to_producer: extra consumer args are OK (producer ignores them) // - producer_to_consumer: this shouldn't happen for arguments (arguments go consumer → producer) // - bidirectional: must match exactly for (const provided of providedArgs) { if (!definedArgs.includes(provided)) { // For consumer_to_producer direction, extra consumer fields are acceptable (downgrade to warning) const isError = direction !== 'consumer_to_producer'; // Check for similar names (potential typo or naming convention mismatch) const similar = findSimilarName(provided, definedArgs); if (isError) { mismatches.push({ toolName: consumer.toolName, issueType: 'ARGUMENT_ERROR', description: similar ? `Unknown argument "${provided}" - did you mean "${similar}"?` : `Unknown argument "${provided}" not in tool schema`, producerLocation: producer.location, consumerLocation: consumer.callSite, details: { unknownArg: provided, definedArgs, suggestedArg: similar }, }); } else { // In consumer_to_producer mode, this is just informational // (We don't have a warnings array in current schema, so skip for now) // TODO: Add warnings support } } } return mismatches; } /** * Check if consumer expects properties that producer doesn't provide * This represents producer → consumer data flow (response direction) * This is the core mismatch detection (e.g., expecting "characterClass" but producer has "class") */ function checkPropertyMismatches( producer: ProducerSchema, consumer: ConsumerSchema, options: CompareOptions, direction: DataFlowDirection ): Mismatch[] { const mismatches: Mismatch[] = []; const ignoreProps = options.ignoreProperties || []; // For now, we can't fully know the output schema structure // But we can detect common patterns and warn about potential issues for (const expectedProp of consumer.expectedProperties) { if (ignoreProps.includes(expectedProp)) continue; // Check for common naming convention mismatches const potentialMatch = detectNamingMismatch(expectedProp, producer); if (potentialMatch) { // Property mismatches in producer → consumer direction: // - producer_to_consumer: consumer expects property producer doesn't have = ERROR // - consumer_to_producer: not applicable (this checks response properties) // - bidirectional: must match = ERROR const isError = direction !== 'consumer_to_producer'; if (isError) { mismatches.push({ toolName: consumer.toolName, issueType: 'MISSING_PROPERTY', description: `Consumer expects "${expectedProp}" but producer has "${potentialMatch}"`, producerLocation: producer.location, consumerLocation: consumer.callSite, details: { expected: expectedProp, actual: potentialMatch, suggestion: `Change "${expectedProp}" to "${potentialMatch}" in consumer code`, direction: direction, }, }); } } } return mismatches; } /** * Detect if expected property has a naming convention mismatch */ function detectNamingMismatch(expectedProp: string, producer: ProducerSchema): string | null { // Common patterns to check const patterns: Array<[RegExp, (match: RegExpMatchArray) => string]> = [ // characterClass vs class [/^(.+)Class$/, (m) => m[1].toLowerCase()], [/^(.+)_class$/, (m) => m[1]], // className vs class [/^class(.+)$/, (m) => m[1].toLowerCase()], // charClass vs class [/^char(.+)$/i, (m) => m[1].toLowerCase()], ]; // Check if expected property follows a pattern that suggests a simpler name in producer for (const [pattern, transform] of patterns) { const match = expectedProp.match(pattern); if (match) { const simpleName = transform(match); // Check if producer's input schema has the simpler name // (This is a heuristic - in reality we'd check the output) if (producer.inputSchema.properties?.[simpleName]) { return simpleName; } // Also check common variations if (simpleName === 'class') { return 'class'; // Known naming conflict with JS reserved word } } } return null; } /** * Find a similar name in a list (for typo detection) */ function findSimilarName(name: string, candidates: string[]): string | null { const nameLower = name.toLowerCase(); for (const candidate of candidates) { const candidateLower = candidate.toLowerCase(); // Exact match (different case) if (nameLower === candidateLower) { return candidate; } // One is a suffix/prefix of the other if (nameLower.includes(candidateLower) || candidateLower.includes(nameLower)) { return candidate; } // Levenshtein distance <= 2 if (levenshteinDistance(nameLower, candidateLower) <= 2) { return candidate; } } return null; } /** * Simple Levenshtein distance for typo detection */ function levenshteinDistance(a: string, b: string): number { const matrix: number[][] = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b[i - 1] === a[j - 1]) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[b.length][a.length]; } /** * Quick check: does producer provide all properties consumer expects? */ export function checkPropertyCoverage( producer: ProducerSchema, consumer: ConsumerSchema ): Mismatch[] { return checkPropertyMismatches(producer, consumer, {}, 'producer_to_consumer'); } /** * Convenience function: Compare a backend directory against a frontend directory */ export async function compareDirectories( backendDir: string, frontendDir: string, options: CompareOptions = {} ): Promise<TraceResult> { // Import dynamically to avoid circular deps const { extractProducerSchemas } = await import('../extract/index.js'); const { traceConsumerUsage } = await import('../trace/index.js'); console.log(`\n[Compare] Backend: ${backendDir}`); console.log(`[Compare] Frontend: ${frontendDir}\n`); const producers = await extractProducerSchemas({ rootDir: backendDir }); const consumers = await traceConsumerUsage({ rootDir: frontendDir }); return compareSchemas(producers, consumers, options); }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/mnehmos.trace.mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server