Skip to main content
Glama
tool-registry-security.test.ts7.57 kB
/** * @fileoverview Tests for tool registry security validation and edge cases */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ToolRegistry, ToolDefinition } from '../../server/tool-registry.js'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { BaseHandlerDeps } from '../../handlers/base/handler.interface.js'; // Mock fs module vi.mock('fs', () => ({ promises: { readdir: vi.fn(), stat: vi.fn(), access: vi.fn(), }, })); // Mock dynamic import const mockDynamicImport = vi.fn(); // @ts-expect-error - Override global import for testing global.import = mockDynamicImport; // Type for accessing private static methods in tests type ToolRegistryStatic = typeof ToolRegistry & { matchesPattern: (filename: string, patterns: string[]) => boolean; passesFilters: ( tool: { metadata?: { category?: string; tags?: string[] } }, options: { includeCategories?: string[]; excludeCategories?: string[]; includeTags?: string[]; excludeTags?: string[]; } ) => boolean; }; describe('ToolRegistry Security and Edge Cases', () => { // Note: This file now only tests public API methods and static helpers // Private method tests have been removed as they are implementation details let mockServer: McpServer; let mockDeps: BaseHandlerDeps; let registry: ToolRegistry; beforeEach(() => { mockServer = { registerTool: vi.fn(), connect: vi.fn(), } as unknown as McpServer; mockDeps = { getApiKey: vi.fn(() => 'test-api-key'), clientFactory: {} as BaseHandlerDeps['clientFactory'], projectRepository: {} as BaseHandlerDeps['projectRepository'], analysisRunRepository: {} as BaseHandlerDeps['analysisRunRepository'], metricsRepository: {} as BaseHandlerDeps['metricsRepository'], complianceReportRepository: {} as BaseHandlerDeps['complianceReportRepository'], logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, }; registry = new ToolRegistry(mockServer, mockDeps); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('discoverTools - Security and Filtering', () => { it('should handle file system errors gracefully', async () => { const fs = vi.mocked(await import('fs')); fs.promises.readdir.mockRejectedValueOnce(new Error('Permission denied')); const result = await registry.discoverTools({ directories: ['./protected-dir'], patterns: ['*.tool.js'], }); expect(result).toEqual([]); // The error is caught internally but should return empty array }); it('should return empty array for non-existent directories', async () => { const fs = vi.mocked(await import('fs')); fs.promises.readdir.mockRejectedValueOnce(new Error('ENOENT: no such file or directory')); const result = await registry.discoverTools({ directories: ['./nonexistent'], patterns: ['*.tool.js'], }); expect(result).toEqual([]); }); }); describe('passesFilters - Edge Cases', () => { it('should handle tools without metadata', () => { const tool: ToolDefinition = { name: 'test-tool', description: 'Test tool', handler: vi.fn(), }; const passesFilters = (ToolRegistry as unknown as ToolRegistryStatic).passesFilters; // Should pass when no filters expect(passesFilters(tool, {})).toBe(true); // Should fail when category filter exists but tool has no metadata expect(passesFilters(tool, { includeCategories: ['api'] })).toBe(false); // Should pass when exclude filter exists but tool has no metadata expect(passesFilters(tool, { excludeCategories: ['api'] })).toBe(true); }); it('should handle empty filter arrays', () => { const tool: ToolDefinition = { name: 'test-tool', description: 'Test tool', handler: vi.fn(), metadata: { category: 'data', tags: ['stable'], }, }; const passesFilters = (ToolRegistry as unknown as ToolRegistryStatic).passesFilters; // Empty arrays should not filter anything expect(passesFilters(tool, { includeCategories: [] })).toBe(true); expect(passesFilters(tool, { excludeCategories: [] })).toBe(true); expect(passesFilters(tool, { includeTags: [] })).toBe(true); expect(passesFilters(tool, { excludeTags: [] })).toBe(true); }); }); describe('matchesPattern - Pattern Matching', () => { it('should match exact filenames', () => { const matchesPattern = (ToolRegistry as unknown as ToolRegistryStatic).matchesPattern; expect(matchesPattern('test.tool.js', ['test.tool.js'])).toBe(true); expect(matchesPattern('test.tool.js', ['other.tool.js'])).toBe(false); }); it('should match wildcard patterns', () => { const matchesPattern = (ToolRegistry as unknown as ToolRegistryStatic).matchesPattern; expect(matchesPattern('test.tool.js', ['*.tool.js'])).toBe(true); expect(matchesPattern('test.tool.ts', ['*.tool.js'])).toBe(false); expect(matchesPattern('my-test.tool.js', ['*-test.tool.js'])).toBe(true); expect(matchesPattern('test.js', ['*.tool.js'])).toBe(false); }); it('should match multiple patterns', () => { const matchesPattern = (ToolRegistry as unknown as ToolRegistryStatic).matchesPattern; expect(matchesPattern('test.tool.js', ['*.tool.js', '*.tool.ts'])).toBe(true); expect(matchesPattern('test.tool.ts', ['*.tool.js', '*.tool.ts'])).toBe(true); expect(matchesPattern('test.js', ['*.tool.js', '*.tool.ts'])).toBe(false); }); it('should handle special regex characters in patterns', () => { const matchesPattern = (ToolRegistry as unknown as ToolRegistryStatic).matchesPattern; // Dots should be treated literally except after * expect(matchesPattern('test.tool.js', ['test.tool.js'])).toBe(true); expect(matchesPattern('testXtoolXjs', ['test.tool.js'])).toBe(false); // Brackets should be escaped expect(matchesPattern('[test].tool.js', ['[test].tool.js'])).toBe(true); expect(matchesPattern('t.tool.js', ['[test].tool.js'])).toBe(false); }); }); describe('Tool Registration', () => { it('should register a tool with proper validation', () => { const tool: ToolDefinition = { name: 'secure-tool', description: 'A secure tool', handler: vi.fn(), metadata: { category: 'security', tags: ['validated'], }, }; registry.registerTool(tool); expect(mockServer.registerTool).toHaveBeenCalledWith( 'secure-tool', expect.objectContaining({ description: 'A secure tool', }), expect.any(Function) ); }); it('should get tools by category with security filtering', () => { const tools: ToolDefinition[] = [ { name: 'tool1', description: 'Tool 1', handler: vi.fn(), metadata: { category: 'secure' }, }, { name: 'tool2', description: 'Tool 2', handler: vi.fn(), metadata: { category: 'unsafe' }, }, ]; tools.forEach((tool) => registry.registerTool(tool)); const secureTooks = registry.getToolsByCategory('secure'); expect(secureTooks).toEqual(['tool1']); }); }); });

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/sapientpants/deepsource-mcp-server'

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