Skip to main content
Glama
tool-catalog-resource.test.ts12.1 kB
/** * Unit tests for tool-catalog-resource.ts * Tests tool catalog generation, filtering, and caching */ import { URLSearchParams } from 'url'; import { describe, it, expect, beforeAll, _beforeEach, _afterEach, _jest } from 'vitest'; // Mock conditional-request (needed for generateETag) vi.mock('../../src/utils/conditional-request.js', () => ({ generateStrongETag: vi.fn((data: any) => `etag-${JSON.stringify(data).length}`), })); // Mock ResourceCache const mockCacheGet = vi.fn(); const mockCacheSet = vi.fn(); vi.mock('../../src/resources/resource-cache.js', () => ({ resourceCache: { get: mockCacheGet, set: mockCacheSet, }, generateETag: (data: any) => `etag-${JSON.stringify(data).length}`, ResourceCache: vi.fn(), })); // Mock tool-catalog const mockGetCatalogSummary = vi.fn(); const mockSearchTools = vi.fn(); const mockToolCatalog = new Map(); vi.mock('../../src/tools/tool-catalog.js', () => ({ TOOL_CATALOG: mockToolCatalog, getCatalogSummary: mockGetCatalogSummary, searchTools: mockSearchTools, })); describe('Tool Catalog Resource', () => { let generateToolCatalogResource: any; beforeAll(async () => { const module = await import('../../src/resources/tool-catalog-resource.js'); generateToolCatalogResource = module.generateToolCatalogResource; }); beforeEach(() => { vi.clearAllMocks(); // Default: no cache hit mockCacheGet.mockResolvedValue(null); // Mock catalog summary mockGetCatalogSummary.mockReturnValue({ totalTools: 60, byCategory: { analysis: 10, adr: 15, deployment: 8, research: 7, memory: 5, 'file-system': 4, 'content-security': 4, rules: 3, workflow: 2, utility: 2, }, cemcpEnabled: 25, highTokenCost: 12, }); // Mock search tools result mockSearchTools.mockReturnValue({ tools: [ { name: 'analyze_project_ecosystem', shortDescription: 'Analyze project structure', fullDescription: 'Comprehensive analysis of project ecosystem', category: 'analysis', complexity: 'complex', tokenCost: { min: 5000, max: 15000 }, hasCEMCPDirective: true, requiresAI: true, relatedTools: ['get_architectural_context'], keywords: ['analysis', 'ecosystem', 'project'], inputSchema: { type: 'object', properties: {} }, }, { name: 'suggest_adrs', shortDescription: 'Suggest ADRs for decisions', fullDescription: 'Generate ADR suggestions based on context', category: 'adr', complexity: 'moderate', tokenCost: { min: 2000, max: 5000 }, hasCEMCPDirective: false, requiresAI: true, relatedTools: ['generate_adr_from_decision'], keywords: ['adr', 'suggestion', 'decision'], inputSchema: { type: 'object', properties: {} }, }, ], totalCount: 2, categories: { analysis: 1, adr: 1, deployment: 0, research: 0, memory: 0, 'file-system': 0, 'content-security': 0, rules: 0, workflow: 0, utility: 0, }, }); }); afterEach(() => { vi.restoreAllMocks(); }); describe('Basic Resource Generation', () => { it('should generate tool catalog with default parameters', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result).toBeDefined(); expect(result.data).toBeDefined(); expect(result.data.summary).toBeDefined(); expect(result.data.tools).toBeDefined(); expect(result.data.summary.totalTools).toBe(60); expect(result.contentType).toBe('application/json'); }); it('should include timestamp in response', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.timestamp).toBeDefined(); expect(new Date(result.data.timestamp).getTime()).toBeGreaterThan(0); }); it('should call getCatalogSummary', async () => { await generateToolCatalogResource({}, new URLSearchParams()); expect(mockGetCatalogSummary).toHaveBeenCalled(); }); it('should call searchTools with default parameters', async () => { await generateToolCatalogResource({}, new URLSearchParams()); expect(mockSearchTools).toHaveBeenCalledWith({ includeSchema: false, limit: 1000, }); }); }); describe('Query Parameter Handling', () => { it('should filter by category when specified', async () => { await generateToolCatalogResource({}, new URLSearchParams('category=adr')); expect(mockSearchTools).toHaveBeenCalledWith({ category: 'adr', includeSchema: false, limit: 1000, }); }); it('should include schemas when requested', async () => { const result = await generateToolCatalogResource( {}, new URLSearchParams('includeSchema=true') ); expect(mockSearchTools).toHaveBeenCalledWith({ includeSchema: true, limit: 1000, }); // Check that schemas are included in tools expect(result.data.tools[0]).toHaveProperty('inputSchema'); }); it('should return lightweight catalog when requested', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams('lightweight=true')); // Lightweight tools should only have basic fields const tool = result.data.tools[0]; expect(tool).toHaveProperty('name'); expect(tool).toHaveProperty('description'); expect(tool).toHaveProperty('category'); expect(tool).toHaveProperty('hasCEMCPDirective'); // Should NOT have these fields expect(tool).not.toHaveProperty('complexity'); expect(tool).not.toHaveProperty('tokenCost'); expect(tool).not.toHaveProperty('relatedTools'); }); it('should use short description for lightweight catalog', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams('lightweight=true')); expect(result.data.tools[0].description).toBe('Analyze project structure'); }); }); describe('Tool Data Structure', () => { it('should include full tool metadata by default', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); const tool = result.data.tools[0]; expect(tool.name).toBe('analyze_project_ecosystem'); expect(tool.description).toBe('Comprehensive analysis of project ecosystem'); expect(tool.category).toBe('analysis'); expect(tool.complexity).toBe('complex'); expect(tool.tokenCost).toEqual({ min: 5000, max: 15000 }); expect(tool.hasCEMCPDirective).toBe(true); expect(tool.requiresAI).toBe(true); expect(tool.relatedTools).toEqual(['get_architectural_context']); expect(tool.keywords).toEqual(['analysis', 'ecosystem', 'project']); }); it('should exclude schemas by default', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.tools[0]).not.toHaveProperty('inputSchema'); }); it('should include all tools from search results', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.tools).toHaveLength(2); expect(result.data.tools[0].name).toBe('analyze_project_ecosystem'); expect(result.data.tools[1].name).toBe('suggest_adrs'); }); }); describe('Summary Statistics', () => { it('should include complete summary', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.summary).toMatchObject({ totalTools: 60, cemcpEnabled: 25, highTokenCost: 12, }); }); it('should include category breakdown', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.summary.byCategory).toBeDefined(); expect(result.data.summary.byCategory.analysis).toBe(10); expect(result.data.summary.byCategory.adr).toBe(15); }); }); describe('Caching', () => { it('should return cached result when available', async () => { const cachedResult = { data: { summary: { totalTools: 50 }, tools: [], timestamp: new Date().toISOString(), }, contentType: 'application/json', cacheKey: 'tool-catalog', }; mockCacheGet.mockResolvedValue(cachedResult); const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result).toBe(cachedResult); expect(mockSearchTools).not.toHaveBeenCalled(); }); it('should cache result after generation', async () => { await generateToolCatalogResource({}, new URLSearchParams()); expect(mockCacheSet).toHaveBeenCalledWith( 'tool-catalog', expect.objectContaining({ data: expect.any(Object), contentType: 'application/json', cacheKey: 'tool-catalog', ttl: 300, }), 300 ); }); it('should use category-specific cache key when filtering', async () => { await generateToolCatalogResource({}, new URLSearchParams('category=adr')); expect(mockCacheSet).toHaveBeenCalledWith('tool-catalog:adr', expect.anything(), 300); }); it('should use 5-minute TTL for relatively static data', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.ttl).toBe(300); }); }); describe('Error Handling', () => { it('should throw error when catalog generation fails', async () => { mockGetCatalogSummary.mockImplementation(() => { throw new Error('Catalog generation failed'); }); await expect(generateToolCatalogResource({}, new URLSearchParams())).rejects.toThrow( 'Failed to generate tool catalog' ); }); it('should throw error when search fails', async () => { mockSearchTools.mockImplementation(() => { throw new Error('Search failed'); }); await expect(generateToolCatalogResource({}, new URLSearchParams())).rejects.toThrow( 'Failed to generate tool catalog' ); }); }); describe('Response Structure', () => { it('should return properly structured ResourceGenerationResult', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result).toMatchObject({ data: expect.objectContaining({ summary: expect.any(Object), tools: expect.any(Array), timestamp: expect.any(String), }), contentType: 'application/json', lastModified: expect.any(String), cacheKey: 'tool-catalog', ttl: 300, etag: expect.any(String), }); }); it('should generate valid etag', async () => { const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.etag).toBeDefined(); expect(typeof result.etag).toBe('string'); expect(result.etag.length).toBeGreaterThan(0); }); }); describe('Edge Cases', () => { it('should handle empty tool list', async () => { mockSearchTools.mockReturnValue({ tools: [], totalCount: 0, categories: {}, }); const result = await generateToolCatalogResource({}, new URLSearchParams()); expect(result.data.tools).toHaveLength(0); }); it('should handle category with no tools', async () => { mockSearchTools.mockReturnValue({ tools: [], totalCount: 0, categories: { workflow: 0, }, }); const result = await generateToolCatalogResource( {}, new URLSearchParams('category=workflow') ); expect(result.data.tools).toHaveLength(0); }); }); });

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/tosin2013/mcp-adr-analysis-server'

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