Skip to main content
Glama
DescribeToolsTool.test.ts32 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DescribeToolsTool } from '../../src/tools/DescribeToolsTool'; import type { McpClientManagerService } from '../../src/services/McpClientManagerService'; import type { SkillService } from '../../src/services/SkillService'; import type { McpClientConnection, Skill, PromptConfig } from '../../src/types'; /** * Text content shape returned by tool execution */ interface TextContent { type: string; text: string; } /** * Type guard to check if a value is a TextContent object */ function isTextContent(value: unknown): value is TextContent { return ( typeof value === 'object' && value !== null && 'type' in value && 'text' in value && typeof (value as TextContent).type === 'string' && typeof (value as TextContent).text === 'string' ); } /** * Helper to extract text from tool result content */ function getTextFromResult(result: { content: unknown[] }): string { const content = result.content[0]; if (!isTextContent(content)) { throw new Error('Expected TextContent in result'); } return content.text; } /** * Tool definition shape for mock clients */ interface MockToolDefinition { name: string; description?: string; inputSchema?: Record<string, unknown>; } /** * Options for creating mock clients */ interface MockClientOptions { serverInstruction?: string; toolBlacklist?: string[]; omitToolDescription?: boolean; } /** * Creates a mock MCP client connection */ function createMockClient( serverName: string, tools: MockToolDefinition[], options: MockClientOptions = {} ): McpClientConnection { return { serverName, serverInstruction: options.serverInstruction, toolBlacklist: options.toolBlacklist, omitToolDescription: options.omitToolDescription, transport: 'stdio', listTools: vi.fn().mockResolvedValue( tools.map((t) => ({ name: t.name, description: t.description || `Description for ${t.name}`, inputSchema: t.inputSchema || { type: 'object', properties: {} }, })) ), listResources: vi.fn().mockResolvedValue([]), listPrompts: vi.fn().mockResolvedValue([]), callTool: vi.fn(), readResource: vi.fn(), getPrompt: vi.fn(), close: vi.fn(), }; } /** * Creates a mock MCP client connection with prompt-based skill configuration */ function createMockClientWithPrompts( serverName: string, tools: MockToolDefinition[], prompts: Record<string, PromptConfig>, options: MockClientOptions = {} ): McpClientConnection { return { serverName, serverInstruction: options.serverInstruction, toolBlacklist: options.toolBlacklist, omitToolDescription: options.omitToolDescription, prompts, transport: 'stdio', listTools: vi.fn().mockResolvedValue( tools.map((t) => ({ name: t.name, description: t.description || `Description for ${t.name}`, inputSchema: t.inputSchema || { type: 'object', properties: {} }, })) ), listResources: vi.fn().mockResolvedValue([]), listPrompts: vi.fn().mockResolvedValue([]), callTool: vi.fn(), readResource: vi.fn(), getPrompt: vi.fn().mockImplementation((promptName: string) => { return Promise.resolve({ messages: [ { role: 'user', content: { type: 'text', text: `Instructions for prompt: ${promptName}` }, }, ], }); }), close: vi.fn(), }; } /** * Creates a mock skill */ function createMockSkill( name: string, description: string, location: 'project' | 'user' = 'project' ): Skill { return { name, description, location, content: `# ${name} skill content`, basePath: `/path/to/${name}`, }; } describe('DescribeToolsTool', () => { let mockClientManager: McpClientManagerService; let mockSkillService: SkillService; beforeEach(() => { mockClientManager = { getAllClients: vi.fn().mockReturnValue([]), getClient: vi.fn(), addClient: vi.fn(), removeClient: vi.fn(), } as unknown as McpClientManagerService; mockSkillService = { getSkills: vi.fn().mockResolvedValue([]), getSkill: vi.fn(), clearCache: vi.fn(), } as unknown as SkillService; }); describe('constructor', () => { it('should create instance with client manager only', () => { const tool = new DescribeToolsTool(mockClientManager); expect(tool).toBeDefined(); }); it('should create instance with client manager and skill service', () => { const tool = new DescribeToolsTool(mockClientManager, mockSkillService); expect(tool).toBeDefined(); }); }); describe('getDefinition', () => { it('should return tool definition with correct name', async () => { const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.name).toBe('describe_tools'); expect(definition.inputSchema).toBeDefined(); expect(definition.inputSchema.properties.toolNames).toBeDefined(); }); it('should include MCP servers in description', async () => { const mockClient = createMockClient('test-server', [ { name: 'tool_one', description: 'First tool' }, { name: 'tool_two', description: 'Second tool' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('test-server'); expect(definition.description).toContain('tool_one'); expect(definition.description).toContain('tool_two'); }); it('should include skills in description when available', async () => { vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('pdf-generator', 'Generate PDF documents'), createMockSkill('code-reviewer', 'Review code changes'), ]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('pdf-generator'); expect(definition.description).toContain('code-reviewer'); }); it('should prefix tool names when they clash across servers', async () => { const mockClient1 = createMockClient('server-a', [ { name: 'shared_tool', description: 'Tool from server A' }, ]); const mockClient2 = createMockClient('server-b', [ { name: 'shared_tool', description: 'Tool from server B' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient1, mockClient2]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('server-a__shared_tool'); expect(definition.description).toContain('server-b__shared_tool'); }); it('should not prefix unique tool names', async () => { const mockClient1 = createMockClient('server-a', [{ name: 'unique_tool_a' }]); const mockClient2 = createMockClient('server-b', [{ name: 'unique_tool_b' }]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient1, mockClient2]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('unique_tool_a'); expect(definition.description).toContain('unique_tool_b'); expect(definition.description).not.toContain('server-a__unique_tool_a'); expect(definition.description).not.toContain('server-b__unique_tool_b'); }); }); describe('skill prefix behavior', () => { it('should NOT prefix skills when they do not clash with MCP tools', async () => { vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('unique-skill', 'A unique skill'), ]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('unique-skill'); expect(definition.description).not.toContain('skill__unique-skill'); }); it('should prefix skills when they clash with MCP tool names', async () => { const mockClient = createMockClient('test-server', [ { name: 'shared-name', description: 'MCP tool' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('shared-name', 'Skill with same name as tool'), ]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // The skill should be prefixed since it clashes with an MCP tool expect(definition.description).toContain('skill__shared-name'); }); it('should NOT prefix skills when multiple skills have the same name (first wins)', async () => { vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('duplicate-skill', 'First duplicate skill', 'project'), createMockSkill('duplicate-skill', 'Second duplicate skill', 'user'), ]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // Skills are de-duplicated (first wins) and NOT prefixed since no MCP tool clash expect(definition.description).toContain('name="duplicate-skill"'); expect(definition.description).not.toContain('skill__duplicate-skill'); }); it('should handle mix of clashing and non-clashing skills', async () => { const mockClient = createMockClient('test-server', [ { name: 'tool-name', description: 'MCP tool' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('tool-name', 'Clashing skill'), createMockSkill('unique-skill', 'Non-clashing skill'), ]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // Clashing skill should be prefixed expect(definition.description).toContain('skill__tool-name'); // Non-clashing skill should NOT be prefixed expect(definition.description).toContain('unique-skill'); expect(definition.description).not.toContain('skill__unique-skill'); }); }); describe('execute', () => { it('should return error when no tool names provided', async () => { const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: [] }); expect(result.isError).toBe(true); expect(result.content[0]).toHaveProperty('text'); }); it('should return tool descriptions for valid tool names', async () => { const mockClient = createMockClient('test-server', [ { name: 'my_tool', description: 'My tool description', inputSchema: { type: 'object', properties: { arg1: { type: 'string' } } }, }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['my_tool'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.tools).toHaveLength(1); expect(parsed.tools[0].tool.name).toBe('my_tool'); }); it('should return skill descriptions for skill__ prefixed names', async () => { vi.mocked(mockSkillService.getSkill).mockResolvedValue( createMockSkill('my-skill', 'My skill description') ); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__my-skill'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); expect(parsed.skills[0].name).toBe('my-skill'); }); it('should return skill descriptions for plain skill names (without skill__ prefix)', async () => { vi.mocked(mockSkillService.getSkill).mockResolvedValue( createMockSkill('my-skill', 'My skill description') ); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['my-skill'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); expect(parsed.skills[0].name).toBe('my-skill'); }); it('should return notFound for unknown tools', async () => { vi.mocked(mockClientManager.getAllClients).mockReturnValue([]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['unknown_tool'] }); expect(result.isError).toBe(true); }); it('should handle mixed valid and invalid tool names', async () => { const mockClient = createMockClient('test-server', [ { name: 'valid_tool', description: 'Valid tool' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['valid_tool', 'invalid_tool'] }); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.tools).toHaveLength(1); expect(parsed.notFound).toContain('invalid_tool'); }); it('should handle server-prefixed tool names', async () => { const mockClient = createMockClient('my-server', [ { name: 'my_tool', description: 'Tool description' }, ]); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['my-server__my_tool'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.tools).toHaveLength(1); expect(parsed.tools[0].server).toBe('my-server'); }); }); describe('error handling', () => { it('should handle client listTools errors gracefully', async () => { const mockClient = { serverName: 'failing-server', transport: 'stdio', listTools: vi.fn().mockRejectedValue(new Error('Connection failed')), listResources: vi.fn().mockResolvedValue([]), listPrompts: vi.fn().mockResolvedValue([]), callTool: vi.fn(), readResource: vi.fn(), getPrompt: vi.fn(), close: vi.fn(), } as unknown as McpClientConnection; vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); // Should not throw, should handle error gracefully const definition = await tool.getDefinition(); expect(definition).toBeDefined(); }); it('should filter out blacklisted tools', async () => { const mockClient = createMockClient( 'test-server', [ { name: 'allowed_tool', description: 'Allowed' }, { name: 'blocked_tool', description: 'Blocked' }, ], { toolBlacklist: ['blocked_tool'] } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('allowed_tool'); expect(definition.description).not.toContain('blocked_tool'); }); }); describe('prompt-based skills', () => { it('should include prompt-based skills in getDefinition', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [{ name: 'my_tool' }], { 'code-review': { skill: { name: 'code-reviewer', description: 'Review code for best practices', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('code-reviewer'); expect(definition.description).toContain('Review code for best practices'); }); it('should return prompt-based skill content when executing with skill__ prefix', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [], { 'code-review': { skill: { name: 'code-reviewer', description: 'Review code for best practices', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__code-reviewer'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); expect(parsed.skills[0].name).toBe('code-reviewer'); expect(parsed.skills[0].instructions).toContain('Instructions for prompt: code-review'); // Should include command message prefix expect(parsed.skills[0].instructions).toContain('<command-message>The "code-reviewer" skill is loading</command-message>'); }); it('should return prompt-based skill content when executing with plain name', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [], { 'doc-gen': { skill: { name: 'doc-generator', description: 'Generate documentation', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['doc-generator'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); expect(parsed.skills[0].name).toBe('doc-generator'); }); it('should include folder path in location when configured', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [], { 'code-review': { skill: { name: 'code-reviewer', description: 'Review code', folder: './prompts/code-review', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__code-reviewer'] }); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills[0].location).toBe('./prompts/code-review'); }); it('should use prompt reference in location when folder not configured', async () => { const mockClient = createMockClientWithPrompts( 'my-server', [], { 'my-prompt': { skill: { name: 'my-skill', description: 'My skill description', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__my-skill'] }); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills[0].location).toBe('prompt:my-server/my-prompt'); }); it('should handle getPrompt errors gracefully', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [], { 'failing-prompt': { skill: { name: 'failing-skill', description: 'This skill will fail', }, }, } ); // Override getPrompt to throw an error mockClient.getPrompt = vi.fn().mockRejectedValue(new Error('Connection failed')); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__failing-skill'] }); // Should return not found since the prompt fetch failed expect(result.isError).toBe(true); }); it('should handle client not found for prompt-based skill', async () => { const mockClient = createMockClientWithPrompts( 'test-server', [], { 'my-prompt': { skill: { name: 'orphan-skill', description: 'Skill with missing client', }, }, } ); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); // Return undefined to simulate client not found vi.mocked(mockClientManager.getClient).mockReturnValue(undefined); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__orphan-skill'] }); expect(result.isError).toBe(true); }); }); describe('interaction between file-based and prompt-based skills', () => { it('should combine file-based and prompt-based skills in getDefinition', async () => { // Set up file-based skill vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('file-skill', 'A file-based skill'), ]); // Set up prompt-based skill const mockClient = createMockClientWithPrompts('test-server', [], { 'prompt-skill-def': { skill: { name: 'prompt-skill', description: 'A prompt-based skill', }, }, }); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); expect(definition.description).toContain('file-skill'); expect(definition.description).toContain('prompt-skill'); }); it('should prefer file-based skill over prompt-based skill with same name', async () => { // Set up file-based skill vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('shared-skill', 'File-based version'), ]); vi.mocked(mockSkillService.getSkill).mockResolvedValue( createMockSkill('shared-skill', 'File-based version') ); // Set up prompt-based skill with same name const mockClient = createMockClientWithPrompts('test-server', [], { 'shared-prompt': { skill: { name: 'shared-skill', description: 'Prompt-based version', }, }, }); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__shared-skill'] }); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); // File-based skill is checked first, so it should be returned expect(parsed.skills[0].location).toBe('/path/to/shared-skill'); }); it('should fall back to prompt-based skill when file-based skill not found', async () => { // No file-based skill vi.mocked(mockSkillService.getSkills).mockResolvedValue([]); vi.mocked(mockSkillService.getSkill).mockResolvedValue(undefined); // Set up prompt-based skill const mockClient = createMockClientWithPrompts('test-server', [], { 'fallback-prompt': { skill: { name: 'fallback-skill', description: 'Fallback prompt-based skill', }, }, }); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); vi.mocked(mockClientManager.getClient).mockReturnValue(mockClient); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const result = await tool.execute({ toolNames: ['skill__fallback-skill'] }); expect(result.isError).toBeUndefined(); const text = getTextFromResult(result); const parsed = JSON.parse(text); expect(parsed.skills).toHaveLength(1); expect(parsed.skills[0].name).toBe('fallback-skill'); expect(parsed.skills[0].location).toContain('prompt:'); }); it('should NOT prefix skill names when they only clash with each other (first wins)', async () => { // File-based skill and prompt-based skill with same name vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('duplicate-name', 'File-based duplicate'), ]); const mockClient = createMockClientWithPrompts('test-server', [], { 'duplicate-prompt': { skill: { name: 'duplicate-name', description: 'Prompt-based duplicate', }, }, }); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // Skills are de-duplicated (file-based wins) and NOT prefixed since no MCP tool clash expect(definition.description).toContain('name="duplicate-name"'); expect(definition.description).not.toContain('skill__duplicate-name'); }); }); describe('prompt name collision with file-based skill', () => { it('should handle collision where file skill and prompt skill have same name (first wins, no prefix)', async () => { // Set up file-based skill vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('pdf', 'Generate PDF documents from file'), ]); // Set up prompt-based skill with same name const mockClient = createMockClientWithPrompts('doc-server', [], { 'pdf-prompt': { skill: { name: 'pdf', description: 'Generate PDF documents from prompt', }, }, }); vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // Skills are de-duplicated (file-based wins) and NOT prefixed since no MCP tool clash expect(definition.description).toContain('name="pdf"'); expect(definition.description).not.toContain('skill__pdf'); // Only one 'pdf' skill should appear (first wins) const matches = definition.description.match(/name="pdf"/g) || []; expect(matches.length).toBe(1); }); it('should handle collision where prompt skill clashes with MCP tool name', async () => { // Set up MCP tool with name 'analyze' const mockClient: McpClientConnection = { serverName: 'test-server', prompts: { 'analyze-prompt': { skill: { name: 'analyze', description: 'Analyze from prompt', }, }, }, transport: 'stdio', listTools: vi.fn().mockResolvedValue([ { name: 'analyze', description: 'Analyze tool', inputSchema: {} }, ]), listResources: vi.fn().mockResolvedValue([]), listPrompts: vi.fn().mockResolvedValue([]), callTool: vi.fn(), readResource: vi.fn(), getPrompt: vi.fn().mockResolvedValue({ messages: [{ role: 'user', content: { type: 'text', text: 'Analyze prompt' } }], }), close: vi.fn(), }; vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // The skill should be prefixed since it clashes with MCP tool expect(definition.description).toContain('name="skill__analyze"'); // But the tool should NOT be prefixed (it's unique) expect(definition.description).toContain('name="analyze"'); }); it('should correctly handle collisions across file skills, prompt skills, and MCP tools', async () => { // Set up file-based skill vi.mocked(mockSkillService.getSkills).mockResolvedValue([ createMockSkill('unique-file-skill', 'Unique file skill'), createMockSkill('shared-name', 'File skill with shared name'), ]); // Set up MCP client with tool and prompt skill const mockClient: McpClientConnection = { serverName: 'test-server', prompts: { 'unique-prompt': { skill: { name: 'unique-prompt-skill', description: 'Unique prompt skill', }, }, 'shared-prompt': { skill: { name: 'shared-name', description: 'Prompt skill with shared name', }, }, }, transport: 'stdio', listTools: vi.fn().mockResolvedValue([ { name: 'unique-tool', description: 'Unique tool', inputSchema: {} }, ]), listResources: vi.fn().mockResolvedValue([]), listPrompts: vi.fn().mockResolvedValue([]), callTool: vi.fn(), readResource: vi.fn(), getPrompt: vi.fn().mockResolvedValue({ messages: [{ role: 'user', content: { type: 'text', text: 'Content' } }], }), close: vi.fn(), }; vi.mocked(mockClientManager.getAllClients).mockReturnValue([mockClient]); const tool = new DescribeToolsTool(mockClientManager, mockSkillService); const definition = await tool.getDefinition(); // Unique names should NOT be prefixed expect(definition.description).toContain('name="unique-file-skill"'); expect(definition.description).toContain('name="unique-prompt-skill"'); expect(definition.description).toContain('name="unique-tool"'); // Shared name between skills should NOT be prefixed (only MCP tool clashes trigger prefix) // File-based skill wins (first), prompt-based is de-duplicated expect(definition.description).toContain('name="shared-name"'); expect(definition.description).not.toContain('skill__shared-name'); }); }); });

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/AgiFlow/aicode-toolkit'

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