mirror of
https://github.com/samanhappy/mcphub.git
synced 2026-01-10 16:47:56 -05:00
242 lines
7.7 KiB
TypeScript
242 lines
7.7 KiB
TypeScript
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
|
|
// Mock dependencies before importing mcpService
|
|
jest.mock('../../src/services/oauthService.js', () => ({
|
|
initializeAllOAuthClients: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../src/services/oauthClientRegistration.js', () => ({
|
|
registerOAuthClient: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../src/services/mcpOAuthProvider.js', () => ({
|
|
createOAuthProvider: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../src/services/groupService.js', () => ({
|
|
getServersInGroup: jest.fn((groupId: string) => {
|
|
if (groupId === 'test-group') {
|
|
return ['server1', 'server2'];
|
|
}
|
|
if (groupId === 'empty-group') {
|
|
return [];
|
|
}
|
|
return undefined;
|
|
}),
|
|
getServerConfigInGroup: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../src/services/sseService.js', () => ({
|
|
getGroup: jest.fn((sessionId: string) => {
|
|
if (sessionId === 'session-smart') return '$smart';
|
|
if (sessionId === 'session-smart-group') return '$smart/test-group';
|
|
if (sessionId === 'session-smart-empty') return '$smart/empty-group';
|
|
return '';
|
|
}),
|
|
}));
|
|
|
|
jest.mock('../../src/dao/index.js', () => ({
|
|
getServerDao: jest.fn(() => ({
|
|
findById: jest.fn(),
|
|
findAll: jest.fn(() => Promise.resolve([])),
|
|
})),
|
|
}));
|
|
|
|
jest.mock('../../src/services/services.js', () => ({
|
|
getDataService: jest.fn(() => ({
|
|
filterData: (data: any) => data,
|
|
})),
|
|
}));
|
|
|
|
// Mock smartRoutingService to initialize with test functions
|
|
const mockHandleSearchToolsRequest = jest.fn();
|
|
jest.mock('../../src/services/smartRoutingService.js', () => ({
|
|
initSmartRoutingService: jest.fn(),
|
|
handleSearchToolsRequest: mockHandleSearchToolsRequest,
|
|
handleDescribeToolRequest: jest.fn(),
|
|
isSmartRoutingGroup: jest.fn((group: string) => group?.startsWith('$smart')),
|
|
getSmartRoutingTools: jest.fn(async (group: string) => {
|
|
const targetGroup = group?.startsWith('$smart/') ? group.substring(7) : undefined;
|
|
const scopeDescription = targetGroup
|
|
? `servers in the "${targetGroup}" group`
|
|
: 'all available servers';
|
|
|
|
return {
|
|
tools: [
|
|
{
|
|
name: 'search_tools',
|
|
description: `Search for relevant tools across ${scopeDescription}.`,
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { query: { type: 'string' }, limit: { type: 'integer' } },
|
|
required: ['query'],
|
|
},
|
|
},
|
|
{
|
|
name: 'call_tool',
|
|
description: 'Execute a tool by name',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { toolName: { type: 'string' } },
|
|
required: ['toolName'],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}),
|
|
}));
|
|
|
|
jest.mock('../../src/services/vectorSearchService.js', () => ({
|
|
searchToolsByVector: jest.fn(),
|
|
saveToolsAsVectorEmbeddings: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('../../src/config/index.js', () => ({
|
|
loadSettings: jest.fn(),
|
|
expandEnvVars: jest.fn((val: string) => val),
|
|
replaceEnvVars: jest.fn((val: any) => val),
|
|
getNameSeparator: jest.fn(() => '::'),
|
|
default: {
|
|
mcpHubName: 'test-hub',
|
|
mcpHubVersion: '1.0.0',
|
|
},
|
|
}));
|
|
|
|
// Import after mocks are set up
|
|
import { handleListToolsRequest, handleCallToolRequest } from '../../src/services/mcpService.js';
|
|
import { getGroup } from '../../src/services/sseService.js';
|
|
import { handleSearchToolsRequest } from '../../src/services/smartRoutingService.js';
|
|
|
|
describe('MCP Service - Smart Routing with Group Support', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
// Setup mock return for handleSearchToolsRequest
|
|
mockHandleSearchToolsRequest.mockResolvedValue({
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify({ tools: [], guideline: 'test', nextSteps: 'test' }),
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
describe('handleListToolsRequest', () => {
|
|
it('should return search_tools and call_tool for $smart group', async () => {
|
|
const result = await handleListToolsRequest({}, { sessionId: 'session-smart' });
|
|
|
|
expect(result.tools).toHaveLength(2);
|
|
expect(result.tools[0].name).toBe('search_tools');
|
|
expect(result.tools[1].name).toBe('call_tool');
|
|
expect(result.tools[0].description).toContain('all available servers');
|
|
});
|
|
|
|
it('should return filtered tools for $smart/{group} pattern', async () => {
|
|
const result = await handleListToolsRequest({}, { sessionId: 'session-smart-group' });
|
|
|
|
expect(getGroup).toHaveBeenCalledWith('session-smart-group');
|
|
// Note: getServersInGroup is now called inside the mocked getSmartRoutingTools
|
|
|
|
expect(result.tools).toHaveLength(2);
|
|
expect(result.tools[0].name).toBe('search_tools');
|
|
expect(result.tools[1].name).toBe('call_tool');
|
|
expect(result.tools[0].description).toContain('servers in the "test-group" group');
|
|
});
|
|
|
|
it('should handle $smart with empty group', async () => {
|
|
const result = await handleListToolsRequest({}, { sessionId: 'session-smart-empty' });
|
|
|
|
expect(getGroup).toHaveBeenCalledWith('session-smart-empty');
|
|
// Note: getServersInGroup is now called inside the mocked getSmartRoutingTools
|
|
|
|
expect(result.tools).toHaveLength(2);
|
|
expect(result.tools[0].name).toBe('search_tools');
|
|
expect(result.tools[1].name).toBe('call_tool');
|
|
// Should still show group-scoped message even if group is empty
|
|
expect(result.tools[0].description).toContain('servers in the "empty-group" group');
|
|
});
|
|
});
|
|
|
|
describe('handleCallToolRequest - search_tools', () => {
|
|
it('should search across all servers when using $smart', async () => {
|
|
const request = {
|
|
params: {
|
|
name: 'search_tools',
|
|
arguments: {
|
|
query: 'test query',
|
|
limit: 10,
|
|
},
|
|
},
|
|
};
|
|
|
|
await handleCallToolRequest(request, { sessionId: 'session-smart' });
|
|
|
|
// handleSearchToolsRequest should be called with the query, limit, and sessionId
|
|
expect(handleSearchToolsRequest).toHaveBeenCalledWith('test query', 10, 'session-smart');
|
|
});
|
|
|
|
it('should filter servers when using $smart/{group}', async () => {
|
|
const request = {
|
|
params: {
|
|
name: 'search_tools',
|
|
arguments: {
|
|
query: 'test query',
|
|
limit: 10,
|
|
},
|
|
},
|
|
};
|
|
|
|
await handleCallToolRequest(request, { sessionId: 'session-smart-group' });
|
|
|
|
// handleSearchToolsRequest should be called with the sessionId that contains group info
|
|
// The group filtering happens inside handleSearchToolsRequest, not in handleCallToolRequest
|
|
expect(handleSearchToolsRequest).toHaveBeenCalledWith(
|
|
'test query',
|
|
10,
|
|
'session-smart-group',
|
|
);
|
|
});
|
|
|
|
it('should handle empty group in $smart/{group}', async () => {
|
|
const request = {
|
|
params: {
|
|
name: 'search_tools',
|
|
arguments: {
|
|
query: 'test query',
|
|
limit: 10,
|
|
},
|
|
},
|
|
};
|
|
|
|
await handleCallToolRequest(request, { sessionId: 'session-smart-empty' });
|
|
|
|
expect(handleSearchToolsRequest).toHaveBeenCalledWith(
|
|
'test query',
|
|
10,
|
|
'session-smart-empty',
|
|
);
|
|
});
|
|
|
|
it('should validate query parameter', async () => {
|
|
// Mock handleSearchToolsRequest to return an error result when query is missing
|
|
mockHandleSearchToolsRequest.mockImplementationOnce(() => {
|
|
return Promise.reject(new Error('Query parameter is required and must be a string'));
|
|
});
|
|
|
|
const request = {
|
|
params: {
|
|
name: 'search_tools',
|
|
arguments: {
|
|
limit: 10,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await handleCallToolRequest(request, { sessionId: 'session-smart' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Query parameter is required');
|
|
});
|
|
});
|
|
});
|