mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
172 lines
4.5 KiB
TypeScript
172 lines
4.5 KiB
TypeScript
import { describe, expect, test, mock } from 'bun:test'
|
|
import { discoverTools, createCachedToolDiscovery } from '../discovery.js'
|
|
import type { DiscoveryOptions } from '../discovery.js'
|
|
import type { ConnectedMCPServer } from '../types.js'
|
|
import type { McpClientDependencies } from '../interfaces.js'
|
|
|
|
function createMockDeps(): McpClientDependencies {
|
|
return {
|
|
logger: {
|
|
debug: mock(() => {}),
|
|
info: mock(() => {}),
|
|
warn: mock(() => {}),
|
|
error: mock(() => {}),
|
|
},
|
|
httpConfig: {
|
|
getUserAgent: () => 'test-agent/1.0',
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('discoverTools', () => {
|
|
test('returns empty array when capabilities.tools is missing', async () => {
|
|
const result = await discoverTools({
|
|
serverName: 'test',
|
|
client: {} as any,
|
|
capabilities: {},
|
|
deps: createMockDeps(),
|
|
})
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
test('fetches and transforms tools from server', async () => {
|
|
const mockClient = {
|
|
request: mock(() =>
|
|
Promise.resolve({
|
|
tools: [
|
|
{
|
|
name: 'search',
|
|
description: 'Search for items',
|
|
inputSchema: { type: 'object' },
|
|
annotations: { readOnlyHint: true, title: 'Search Items' },
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
}
|
|
|
|
const result = await discoverTools({
|
|
serverName: 'my-server',
|
|
client: mockClient as any,
|
|
capabilities: { tools: {} },
|
|
deps: createMockDeps(),
|
|
})
|
|
|
|
expect(result).toHaveLength(1)
|
|
const tool = result[0]
|
|
expect(tool.name).toBe('mcp__my-server__search')
|
|
expect(tool.mcpInfo).toEqual({
|
|
serverName: 'my-server',
|
|
toolName: 'search',
|
|
})
|
|
expect(tool.isMcp).toBe(true)
|
|
expect(tool.isReadOnly({} as any)).toBe(true)
|
|
expect(tool.userFacingName(undefined)).toBe('Search Items')
|
|
expect(
|
|
await tool.description({} as any, {
|
|
isNonInteractiveSession: false,
|
|
toolPermissionContext: {},
|
|
tools: [],
|
|
}),
|
|
).toBe('Search for items')
|
|
})
|
|
|
|
test('respects skipPrefix option', async () => {
|
|
const mockClient = {
|
|
request: mock(() =>
|
|
Promise.resolve({
|
|
tools: [{ name: 'search', description: 'Search' }],
|
|
}),
|
|
),
|
|
}
|
|
|
|
const result = await discoverTools({
|
|
serverName: 'my-server',
|
|
client: mockClient as any,
|
|
capabilities: { tools: {} },
|
|
skipPrefix: true,
|
|
deps: createMockDeps(),
|
|
})
|
|
|
|
expect(result[0].name).toBe('search')
|
|
})
|
|
|
|
test('returns empty array on fetch error', async () => {
|
|
const mockClient = {
|
|
request: mock(() => Promise.reject(new Error('Connection lost'))),
|
|
}
|
|
const deps = createMockDeps()
|
|
|
|
const result = await discoverTools({
|
|
serverName: 'failing-server',
|
|
client: mockClient as any,
|
|
capabilities: { tools: {} },
|
|
deps,
|
|
})
|
|
|
|
expect(result).toEqual([])
|
|
expect(deps.logger.warn).toHaveBeenCalled()
|
|
})
|
|
|
|
test('sanitizes tool data', async () => {
|
|
const mockClient = {
|
|
request: mock(() =>
|
|
Promise.resolve({
|
|
tools: [
|
|
{
|
|
name: 'tool\x00with\x07control',
|
|
description: 'desc',
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
}
|
|
|
|
const result = await discoverTools({
|
|
serverName: 'test',
|
|
client: mockClient as any,
|
|
capabilities: { tools: {} },
|
|
deps: createMockDeps(),
|
|
})
|
|
|
|
expect(result[0].name).not.toContain('\x00')
|
|
})
|
|
})
|
|
|
|
describe('createCachedToolDiscovery', () => {
|
|
test('caches results by server name', async () => {
|
|
const deps = createMockDeps()
|
|
const { discover, cache } = createCachedToolDiscovery(deps)
|
|
|
|
const mockConn = {
|
|
type: 'connected' as const,
|
|
name: 'cached-server',
|
|
client: {
|
|
request: mock(() =>
|
|
Promise.resolve({
|
|
tools: [{ name: 'tool1', description: 'Tool 1' }],
|
|
}),
|
|
),
|
|
},
|
|
capabilities: { tools: {} },
|
|
} as unknown as ConnectedMCPServer
|
|
|
|
// First call — should fetch
|
|
const result1 = await discover(mockConn)
|
|
expect(result1).toHaveLength(1)
|
|
|
|
// Second call — should use cache
|
|
const result2 = await discover(mockConn)
|
|
expect(result2).toHaveLength(1)
|
|
|
|
// Request was called only once
|
|
expect(mockConn.client.request).toHaveBeenCalledTimes(1)
|
|
|
|
// Cache delete works
|
|
cache.delete('cached-server')
|
|
const result3 = await discover(mockConn)
|
|
expect(result3).toHaveLength(1)
|
|
expect(mockConn.client.request).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|