Skip to main content
Glama
direction-aware.test.ts12 kB
/** * Tests for Direction-Aware Comparison * Tests the new DataFlowDirection feature */ import { describe, it, expect } from 'vitest'; import { compareSchemas } from '../src/compare/index.js'; import type { ProducerSchema, ConsumerSchema } from '../src/types.js'; describe('Direction-Aware Comparison', () => { // Helper to create a minimal producer schema const createProducer = ( toolName: string, inputProps: Record<string, any>, required: string[] = [] ): ProducerSchema => ({ toolName, inputSchema: { type: 'object', properties: inputProps, required, }, outputSchema: { type: 'object', properties: {}, }, location: { file: 'test.ts', line: 1 }, }); // Helper to create a minimal consumer schema const createConsumer = ( toolName: string, args: Record<string, any>, expectedProps: string[] = [] ): ConsumerSchema => ({ toolName, callSite: { file: 'test.ts', line: 1 }, argumentsProvided: args, expectedProperties: expectedProps, }); describe('producer_to_consumer (API Response Pattern)', () => { it('should allow extra producer fields (consumer ignores them)', () => { const producer = createProducer('get_user', { id: { type: 'string' }, // Producer accepts extra fields in input }, ['id']); const consumer = createConsumer('get_user', { id: '123', // Consumer sends only required field }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.mismatches).toHaveLength(0); expect(result.matches).toHaveLength(1); }); it('should error on missing required arguments', () => { const producer = createProducer('get_user', { id: { type: 'string' }, email: { type: 'string' }, }, ['id', 'email']); const consumer = createConsumer('get_user', { id: '123', // Missing 'email' required argument }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); expect(result.mismatches[0].description).toContain('Missing required argument "email"'); }); it('should error on unknown consumer arguments', () => { const producer = createProducer('get_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer('get_user', { id: '123', userId: '456', // Unknown argument }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); expect(result.mismatches[0].description).toContain('Unknown argument "userId"'); }); it('should error when consumer expects property producer does not provide', () => { const producer = createProducer('get_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer( 'get_user', { id: '123' }, ['class'] // Consumer expects 'class' in response ); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); // This should detect a property mismatch if detectNamingMismatch finds a match // Since our sample doesn't have matching properties, it won't create a mismatch // but in a real scenario where naming mismatch is detected, it would error expect(result.matches).toBeDefined(); }); }); describe('consumer_to_producer (API Request Pattern)', () => { it('should allow extra consumer arguments (producer ignores them)', () => { const producer = createProducer('create_user', { name: { type: 'string' }, email: { type: 'string' }, }, ['name', 'email']); const consumer = createConsumer('create_user', { name: 'Alice', email: 'alice@example.com', _csrf: 'token123', // Extra field that producer will ignore }); const result = compareSchemas([producer], [consumer], { direction: 'consumer_to_producer', }); // In consumer_to_producer mode, extra consumer args are OK // (downgraded to warning, but we don't have warnings yet) expect(result.matches).toHaveLength(1); }); it('should still error on missing required arguments', () => { const producer = createProducer('create_user', { name: { type: 'string' }, email: { type: 'string' }, }, ['name', 'email']); const consumer = createConsumer('create_user', { name: 'Alice', // Missing required 'email' }); const result = compareSchemas([producer], [consumer], { direction: 'consumer_to_producer', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); expect(result.mismatches[0].description).toContain('Missing required argument "email"'); }); it('should not error on response property mismatches', () => { const producer = createProducer('get_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer( 'get_user', { id: '123' }, ['avatarUrl'] // Consumer expects property in response ); const result = compareSchemas([producer], [consumer], { direction: 'consumer_to_producer', }); // In consumer_to_producer mode, response property checks are not applicable // So this should not create an error expect(result.matches).toBeDefined(); }); }); describe('bidirectional (Strict Matching)', () => { it('should error on extra consumer arguments', () => { const producer = createProducer('update_user', { id: { type: 'string' }, name: { type: 'string' }, }, ['id', 'name']); const consumer = createConsumer('update_user', { id: '123', name: 'Alice', email: 'alice@example.com', // Extra field }); const result = compareSchemas([producer], [consumer], { direction: 'bidirectional', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); expect(result.mismatches[0].description).toContain('Unknown argument "email"'); }); it('should error on missing required arguments', () => { const producer = createProducer('update_user', { id: { type: 'string' }, name: { type: 'string' }, }, ['id', 'name']); const consumer = createConsumer('update_user', { id: '123', // Missing 'name' }); const result = compareSchemas([producer], [consumer], { direction: 'bidirectional', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); expect(result.mismatches[0].description).toContain('Missing required argument "name"'); }); it('should match when arguments are exact', () => { const producer = createProducer('delete_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer('delete_user', { id: '123', }); const result = compareSchemas([producer], [consumer], { direction: 'bidirectional', }); expect(result.mismatches).toHaveLength(0); expect(result.matches).toHaveLength(1); }); }); describe('Default behavior (backward compatibility)', () => { it('should default to producer_to_consumer when direction not specified', () => { const producer = createProducer('get_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer('get_user', { id: '123', extraField: 'value', // Should error in producer_to_consumer }); const result = compareSchemas([producer], [consumer]); // No direction specified expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('ARGUMENT_ERROR'); }); }); describe('Unknown tool handling', () => { it('should report unknown tools regardless of direction', () => { const producer = createProducer('get_user', { id: { type: 'string' }, }, ['id']); const consumer = createConsumer('delete_user', { id: '123' }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.mismatches).toHaveLength(1); expect(result.mismatches[0].issueType).toBe('UNKNOWN_TOOL'); expect(result.mismatches[0].description).toContain('delete_user'); }); }); describe('Typo detection with direction', () => { it('should suggest similar argument names in producer_to_consumer mode', () => { const producer = createProducer('create_user', { userName: { type: 'string' }, }, ['userName']); const consumer = createConsumer('create_user', { username: 'alice', // Typo: should be userName }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.mismatches).toHaveLength(2); // Missing userName + unknown username const unknownArgError = result.mismatches.find( m => m.description.includes('Unknown argument "username"') ); expect(unknownArgError?.description).toContain('userName'); }); it('should not error on typos in consumer_to_producer mode', () => { const producer = createProducer('create_user', { userName: { type: 'string' }, }, ['userName']); const consumer = createConsumer('create_user', { userName: 'alice', extraField: 'value', // Extra field, should be allowed }); const result = compareSchemas([producer], [consumer], { direction: 'consumer_to_producer', }); // Should match because extra consumer fields are OK expect(result.matches).toHaveLength(1); }); }); describe('Multiple tools with different directions', () => { it('should handle multiple tools correctly', () => { const producers = [ createProducer('get_user', { id: { type: 'string' } }, ['id']), createProducer('create_user', { name: { type: 'string' } }, ['name']), ]; const consumers = [ createConsumer('get_user', { id: '123' }), createConsumer('create_user', { name: 'Alice', extra: 'field' }), ]; const result = compareSchemas(producers, consumers, { direction: 'consumer_to_producer', }); // get_user should match // create_user should match (extra field allowed in consumer_to_producer) expect(result.matches).toHaveLength(2); }); }); describe('Edge cases', () => { it('should handle empty consumer arguments', () => { const producer = createProducer('ping', {}, []); const consumer = createConsumer('ping', {}); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.matches).toHaveLength(1); }); it('should handle optional arguments', () => { const producer = createProducer('search', { query: { type: 'string' }, limit: { type: 'number' }, }, ['query']); // limit is optional const consumer = createConsumer('search', { query: 'test', // Not providing optional 'limit' }); const result = compareSchemas([producer], [consumer], { direction: 'producer_to_consumer', }); expect(result.matches).toHaveLength(1); }); }); });

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