mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Add group-scoped smart routing via $smart/{group} pattern (#388)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
26
README.md
26
README.md
@@ -147,7 +147,11 @@ This endpoint provides a unified streamable HTTP interface for all your MCP serv
|
|||||||
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
|
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# Search across all servers
|
||||||
http://localhost:3000/mcp/$smart
|
http://localhost:3000/mcp/$smart
|
||||||
|
|
||||||
|
# Search within a specific group
|
||||||
|
http://localhost:3000/mcp/$smart/{group}
|
||||||
```
|
```
|
||||||
|
|
||||||
**How it Works:**
|
**How it Works:**
|
||||||
@@ -156,6 +160,7 @@ http://localhost:3000/mcp/$smart
|
|||||||
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity
|
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity
|
||||||
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise
|
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise
|
||||||
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation
|
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation
|
||||||
|
5. **Group Scoping**: Optionally limit searches to servers within a specific group for focused results
|
||||||
|
|
||||||
**Setup Requirements:**
|
**Setup Requirements:**
|
||||||
|
|
||||||
@@ -167,6 +172,23 @@ To enable Smart Routing, you need:
|
|||||||
- OpenAI API key (or compatible embedding service)
|
- OpenAI API key (or compatible embedding service)
|
||||||
- Enable Smart Routing in MCPHub settings
|
- Enable Smart Routing in MCPHub settings
|
||||||
|
|
||||||
|
**Group-Scoped Smart Routing**:
|
||||||
|
|
||||||
|
You can combine Smart Routing with group filtering to search only within specific server groups:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Search only within production servers
|
||||||
|
http://localhost:3000/mcp/$smart/production
|
||||||
|
|
||||||
|
# Search only within development servers
|
||||||
|
http://localhost:3000/mcp/$smart/development
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables:
|
||||||
|
- **Focused Discovery**: Find tools only from relevant servers
|
||||||
|
- **Environment Isolation**: Separate tool discovery by environment (dev, staging, prod)
|
||||||
|
- **Team-Based Access**: Limit tool search to team-specific server groups
|
||||||
|
|
||||||
**Group-Specific Endpoints (Recommended)**:
|
**Group-Specific Endpoints (Recommended)**:
|
||||||
|
|
||||||

|

|
||||||
@@ -205,7 +227,11 @@ http://localhost:3000/sse
|
|||||||
For smart routing, use:
|
For smart routing, use:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# Search across all servers
|
||||||
http://localhost:3000/sse/$smart
|
http://localhost:3000/sse/$smart
|
||||||
|
|
||||||
|
# Search within a specific group
|
||||||
|
http://localhost:3000/sse/$smart/{group}
|
||||||
```
|
```
|
||||||
|
|
||||||
For targeted access to specific server groups, use the group-based SSE endpoint:
|
For targeted access to specific server groups, use the group-based SSE endpoint:
|
||||||
|
|||||||
26
README.zh.md
26
README.zh.md
@@ -145,7 +145,11 @@ http://localhost:3000/mcp
|
|||||||
智能路由是 MCPHub 的智能工具发现系统,使用向量语义搜索自动为任何给定任务找到最相关的工具。
|
智能路由是 MCPHub 的智能工具发现系统,使用向量语义搜索自动为任何给定任务找到最相关的工具。
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 在所有服务器中搜索
|
||||||
http://localhost:3000/mcp/$smart
|
http://localhost:3000/mcp/$smart
|
||||||
|
|
||||||
|
# 在特定分组中搜索
|
||||||
|
http://localhost:3000/mcp/$smart/{group}
|
||||||
```
|
```
|
||||||
|
|
||||||
**工作原理:**
|
**工作原理:**
|
||||||
@@ -154,6 +158,7 @@ http://localhost:3000/mcp/$smart
|
|||||||
2. **语义搜索**:用户查询转换为向量并使用余弦相似度与工具嵌入匹配
|
2. **语义搜索**:用户查询转换为向量并使用余弦相似度与工具嵌入匹配
|
||||||
3. **智能筛选**:动态阈值确保相关结果且无噪声
|
3. **智能筛选**:动态阈值确保相关结果且无噪声
|
||||||
4. **精确执行**:找到的工具可以直接执行并进行适当的参数验证
|
4. **精确执行**:找到的工具可以直接执行并进行适当的参数验证
|
||||||
|
5. **分组限定**:可选择将搜索限制在特定分组的服务器内以获得更精确的结果
|
||||||
|
|
||||||
**设置要求:**
|
**设置要求:**
|
||||||
|
|
||||||
@@ -165,6 +170,23 @@ http://localhost:3000/mcp/$smart
|
|||||||
- OpenAI API 密钥(或兼容的嵌入服务)
|
- OpenAI API 密钥(或兼容的嵌入服务)
|
||||||
- 在 MCPHub 设置中启用智能路由
|
- 在 MCPHub 设置中启用智能路由
|
||||||
|
|
||||||
|
**分组限定的智能路由**:
|
||||||
|
|
||||||
|
您可以将智能路由与分组筛选结合使用,仅在特定服务器分组内搜索:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 仅在生产服务器中搜索
|
||||||
|
http://localhost:3000/mcp/$smart/production
|
||||||
|
|
||||||
|
# 仅在开发服务器中搜索
|
||||||
|
http://localhost:3000/mcp/$smart/development
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以实现:
|
||||||
|
- **精准发现**:仅从相关服务器查找工具
|
||||||
|
- **环境隔离**:按环境(开发、测试、生产)分离工具发现
|
||||||
|
- **基于团队的访问**:将工具搜索限制在特定团队的服务器分组
|
||||||
|
|
||||||
**基于分组的 HTTP 端点(推荐)**:
|
**基于分组的 HTTP 端点(推荐)**:
|
||||||

|

|
||||||
要针对特定服务器分组进行访问,请使用基于分组的 HTTP 端点:
|
要针对特定服务器分组进行访问,请使用基于分组的 HTTP 端点:
|
||||||
@@ -203,7 +225,11 @@ http://localhost:3000/sse
|
|||||||
要启用智能路由,请使用:
|
要启用智能路由,请使用:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 在所有服务器中搜索
|
||||||
http://localhost:3000/sse/$smart
|
http://localhost:3000/sse/$smart
|
||||||
|
|
||||||
|
# 在特定分组中搜索
|
||||||
|
http://localhost:3000/sse/$smart/{group}
|
||||||
```
|
```
|
||||||
|
|
||||||
要针对特定服务器分组进行访问,请使用基于分组的 SSE 端点:
|
要针对特定服务器分组进行访问,请使用基于分组的 SSE 端点:
|
||||||
|
|||||||
@@ -276,17 +276,92 @@ Access Smart Routing through the special `$smart` endpoint:
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="HTTP MCP">
|
<Tab title="HTTP MCP">
|
||||||
```
|
```
|
||||||
|
# Search across all servers
|
||||||
http://localhost:3000/mcp/$smart
|
http://localhost:3000/mcp/$smart
|
||||||
|
|
||||||
|
# Search within a specific group
|
||||||
|
http://localhost:3000/mcp/$smart/{group}
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="SSE (Legacy)">
|
<Tab title="SSE (Legacy)">
|
||||||
```
|
```
|
||||||
|
# Search across all servers
|
||||||
http://localhost:3000/sse/$smart
|
http://localhost:3000/sse/$smart
|
||||||
|
|
||||||
|
# Search within a specific group
|
||||||
|
http://localhost:3000/sse/$smart/{group}
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
### Group-Scoped Smart Routing
|
||||||
|
|
||||||
|
Smart Routing now supports group-scoped searches, allowing you to limit tool discovery to servers within a specific group:
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Using Group-Scoped Smart Routing">
|
||||||
|
Connect your AI client to a group-specific Smart Routing endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:3000/mcp/$smart/production
|
||||||
|
```
|
||||||
|
|
||||||
|
This endpoint will only search for tools within servers that belong to the "production" group.
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- **Focused Results**: Only tools from relevant servers are returned
|
||||||
|
- **Better Performance**: Reduced search space for faster queries
|
||||||
|
- **Environment Isolation**: Keep development, staging, and production tools separate
|
||||||
|
- **Access Control**: Limit tool discovery based on user permissions
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Example: Environment-Based Groups">
|
||||||
|
Create groups for different environments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development environment
|
||||||
|
http://localhost:3000/mcp/$smart/development
|
||||||
|
|
||||||
|
# Staging environment
|
||||||
|
http://localhost:3000/mcp/$smart/staging
|
||||||
|
|
||||||
|
# Production environment
|
||||||
|
http://localhost:3000/mcp/$smart/production
|
||||||
|
```
|
||||||
|
|
||||||
|
Each endpoint will only return tools from servers in that specific environment group.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Example: Team-Based Groups">
|
||||||
|
Organize tools by team or department:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend team tools
|
||||||
|
http://localhost:3000/mcp/$smart/backend-team
|
||||||
|
|
||||||
|
# Frontend team tools
|
||||||
|
http://localhost:3000/mcp/$smart/frontend-team
|
||||||
|
|
||||||
|
# DevOps team tools
|
||||||
|
http://localhost:3000/mcp/$smart/devops-team
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables teams to have focused access to their relevant toolsets.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="How It Works">
|
||||||
|
When using `$smart/{group}`:
|
||||||
|
|
||||||
|
1. The system identifies the specified group
|
||||||
|
2. Retrieves all servers belonging to that group
|
||||||
|
3. Filters the tool search to only those servers
|
||||||
|
4. Returns results scoped to the group's servers
|
||||||
|
|
||||||
|
If the group doesn't exist or has no servers, the search will return no results.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
{/* ### Basic Usage
|
{/* ### Basic Usage
|
||||||
|
|
||||||
Connect your AI client to the Smart Routing endpoint and make natural language requests:
|
Connect your AI client to the Smart Routing endpoint and make natural language requests:
|
||||||
|
|||||||
@@ -889,30 +889,48 @@ export const handleListToolsRequest = async (_: any, extra: any) => {
|
|||||||
console.log(`Handling ListToolsRequest for group: ${group}`);
|
console.log(`Handling ListToolsRequest for group: ${group}`);
|
||||||
|
|
||||||
// Special handling for $smart group to return special tools
|
// Special handling for $smart group to return special tools
|
||||||
if (group === '$smart') {
|
// Support both $smart and $smart/{group} patterns
|
||||||
|
if (group === '$smart' || group?.startsWith('$smart/')) {
|
||||||
|
// Extract target group if pattern is $smart/{group}
|
||||||
|
const targetGroup = group?.startsWith('$smart/') ? group.substring(7) : undefined;
|
||||||
|
|
||||||
|
// Get info about available servers, filtered by target group if specified
|
||||||
|
let availableServers = serverInfos.filter(
|
||||||
|
(server) => server.status === 'connected' && server.enabled !== false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If a target group is specified, filter servers to only those in the group
|
||||||
|
if (targetGroup) {
|
||||||
|
const serversInGroup = getServersInGroup(targetGroup);
|
||||||
|
if (serversInGroup && serversInGroup.length > 0) {
|
||||||
|
availableServers = availableServers.filter((server) =>
|
||||||
|
serversInGroup.includes(server.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create simple server information with only server names
|
||||||
|
const serversList = availableServers
|
||||||
|
.map((server) => {
|
||||||
|
return `${server.name}`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const scopeDescription = targetGroup
|
||||||
|
? `servers in the "${targetGroup}" group`
|
||||||
|
: 'all available servers';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
name: 'search_tools',
|
name: 'search_tools',
|
||||||
description: (() => {
|
description: `STEP 1 of 2: Use this tool FIRST to discover and search for relevant tools across ${scopeDescription}. This tool and call_tool work together as a two-step process: 1) search_tools to find what you need, 2) call_tool to execute it.
|
||||||
// Get info about available servers
|
|
||||||
const availableServers = serverInfos.filter(
|
|
||||||
(server) => server.status === 'connected' && server.enabled !== false,
|
|
||||||
);
|
|
||||||
// Create simple server information with only server names
|
|
||||||
const serversList = availableServers
|
|
||||||
.map((server) => {
|
|
||||||
return `${server.name}`;
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
return `STEP 1 of 2: Use this tool FIRST to discover and search for relevant tools across all available servers. This tool and call_tool work together as a two-step process: 1) search_tools to find what you need, 2) call_tool to execute it.
|
|
||||||
|
|
||||||
For optimal results, use specific queries matching your exact needs. Call this tool multiple times with different queries for different parts of complex tasks. Example queries: "image generation tools", "code review tools", "data analysis", "translation capabilities", etc. Results are sorted by relevance using vector similarity.
|
For optimal results, use specific queries matching your exact needs. Call this tool multiple times with different queries for different parts of complex tasks. Example queries: "image generation tools", "code review tools", "data analysis", "translation capabilities", etc. Results are sorted by relevance using vector similarity.
|
||||||
|
|
||||||
After finding relevant tools, you MUST use the call_tool to actually execute them. The search_tools only finds tools - it doesn't execute them.
|
After finding relevant tools, you MUST use the call_tool to actually execute them. The search_tools only finds tools - it doesn't execute them.
|
||||||
|
|
||||||
Available servers: ${serversList}`;
|
Available servers: ${serversList}`,
|
||||||
})(),
|
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -1029,7 +1047,25 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Using similarity threshold: ${thresholdNum} for query: "${query}"`);
|
console.log(`Using similarity threshold: ${thresholdNum} for query: "${query}"`);
|
||||||
const servers = undefined; // No server filtering
|
|
||||||
|
// Determine server filtering based on group
|
||||||
|
const sessionId = extra.sessionId || '';
|
||||||
|
const group = getGroup(sessionId);
|
||||||
|
let servers: string[] | undefined = undefined; // No server filtering by default
|
||||||
|
|
||||||
|
// If group is in format $smart/{group}, filter servers to that group
|
||||||
|
if (group?.startsWith('$smart/')) {
|
||||||
|
const targetGroup = group.substring(7);
|
||||||
|
const serversInGroup = getServersInGroup(targetGroup);
|
||||||
|
if (serversInGroup !== undefined && serversInGroup !== null) {
|
||||||
|
servers = serversInGroup;
|
||||||
|
if (servers.length > 0) {
|
||||||
|
console.log(`Filtering search to servers in group "${targetGroup}": ${servers.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Group "${targetGroup}" has no servers, search will return no results`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers);
|
const searchResults = await searchToolsByVector(query, limitNum, thresholdNum, servers);
|
||||||
console.log(`Search results: ${JSON.stringify(searchResults)}`);
|
console.log(`Search results: ${JSON.stringify(searchResults)}`);
|
||||||
|
|||||||
222
tests/services/mcpService-smart-routing-group.test.ts
Normal file
222
tests/services/mcpService-smart-routing-group.test.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
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,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 { getServersInGroup } from '../../src/services/groupService.js';
|
||||||
|
import { getGroup } from '../../src/services/sseService.js';
|
||||||
|
import { searchToolsByVector } from '../../src/services/vectorSearchService.js';
|
||||||
|
|
||||||
|
describe('MCP Service - Smart Routing with Group Support', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
expect(getServersInGroup).toHaveBeenCalledWith('test-group');
|
||||||
|
|
||||||
|
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');
|
||||||
|
expect(getServersInGroup).toHaveBeenCalledWith('empty-group');
|
||||||
|
|
||||||
|
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 mockSearchResults = [
|
||||||
|
{
|
||||||
|
serverName: 'server1',
|
||||||
|
toolName: 'server1::tool1',
|
||||||
|
description: 'Test tool 1',
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
(searchToolsByVector as jest.Mock).mockResolvedValue(mockSearchResults);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
params: {
|
||||||
|
name: 'search_tools',
|
||||||
|
arguments: {
|
||||||
|
query: 'test query',
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleCallToolRequest(request, { sessionId: 'session-smart' });
|
||||||
|
|
||||||
|
expect(searchToolsByVector).toHaveBeenCalledWith(
|
||||||
|
'test query',
|
||||||
|
10,
|
||||||
|
expect.any(Number),
|
||||||
|
undefined, // No server filtering
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter servers when using $smart/{group}', async () => {
|
||||||
|
const mockSearchResults = [
|
||||||
|
{
|
||||||
|
serverName: 'server1',
|
||||||
|
toolName: 'server1::tool1',
|
||||||
|
description: 'Test tool 1',
|
||||||
|
inputSchema: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
(searchToolsByVector as jest.Mock).mockResolvedValue(mockSearchResults);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
params: {
|
||||||
|
name: 'search_tools',
|
||||||
|
arguments: {
|
||||||
|
query: 'test query',
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleCallToolRequest(request, { sessionId: 'session-smart-group' });
|
||||||
|
|
||||||
|
expect(getGroup).toHaveBeenCalledWith('session-smart-group');
|
||||||
|
expect(getServersInGroup).toHaveBeenCalledWith('test-group');
|
||||||
|
expect(searchToolsByVector).toHaveBeenCalledWith(
|
||||||
|
'test query',
|
||||||
|
10,
|
||||||
|
expect.any(Number),
|
||||||
|
['server1', 'server2'], // Filtered to group servers
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty group in $smart/{group}', async () => {
|
||||||
|
const mockSearchResults: any[] = [];
|
||||||
|
(searchToolsByVector as jest.Mock).mockResolvedValue(mockSearchResults);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
params: {
|
||||||
|
name: 'search_tools',
|
||||||
|
arguments: {
|
||||||
|
query: 'test query',
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleCallToolRequest(request, { sessionId: 'session-smart-empty' });
|
||||||
|
|
||||||
|
expect(getGroup).toHaveBeenCalledWith('session-smart-empty');
|
||||||
|
expect(getServersInGroup).toHaveBeenCalledWith('empty-group');
|
||||||
|
// Empty group returns empty array, which should still be passed to search
|
||||||
|
expect(searchToolsByVector).toHaveBeenCalledWith(
|
||||||
|
'test query',
|
||||||
|
10,
|
||||||
|
expect.any(Number),
|
||||||
|
[], // Empty group
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate query parameter', async () => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user