@loop_ouroboros/mcp-hub-lite 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -3
- package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
- package/dist/server/src/api/web/hub-tools.js +2 -2
- package/dist/server/src/api/web/search.d.ts +1 -1
- package/dist/server/src/api/web/search.d.ts.map +1 -1
- package/dist/server/src/api/web/search.js +18 -16
- package/dist/server/src/models/system-tools.constants.d.ts +1 -0
- package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +3 -2
- package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/system-tool-definitions.js +7 -0
- package/dist/server/src/services/hub-tools.service.d.ts +1 -1
- package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools.service.js +20 -14
- package/dist/server/src/services/system-tool-handler.js +1 -1
- package/dist/server/src/utils/search-matcher.d.ts +6 -0
- package/dist/server/src/utils/search-matcher.d.ts.map +1 -0
- package/dist/server/src/utils/search-matcher.js +24 -0
- package/dist/server/tests/unit/services/hub-tools.service.test.js +295 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.2] - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Search
|
|
8
|
+
|
|
9
|
+
- add tokenized multi-delimiter search in search_tools with match-count sorting and configurable result limit per server
|
|
10
|
+
|
|
11
|
+
### Docs
|
|
12
|
+
|
|
13
|
+
- clarify stateless mode requires header matching, UA detection is manual
|
|
14
|
+
|
|
5
15
|
## [1.3.1] - 2026-05-31
|
|
6
16
|
|
|
7
17
|
### Gateway
|
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ To connect your MCP client to MCP-HUB-LITE, add the following to your client's M
|
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
Stateful mode provides session persistence, SSE streaming, and real-time notifications.
|
|
260
|
+
Stateful mode provides session persistence, SSE streaming, and real-time notifications. UA matching is NOT automatic — it requires manually adding `"ClaudeCode"` to `system.session.sessionModeRules.stateful` in `.mcp-hub.json`. Standard MCP clients like Claude Code work with the default stateful mode without extra configuration.
|
|
261
261
|
|
|
262
262
|
</details>
|
|
263
263
|
|
|
@@ -269,13 +269,18 @@ Stateful mode provides session persistence, SSE streaming, and real-time notific
|
|
|
269
269
|
"mcpServers": {
|
|
270
270
|
"mcp-hub-lite": {
|
|
271
271
|
"type": "http",
|
|
272
|
-
"url": "http://localhost:7788/mcp"
|
|
272
|
+
"url": "http://localhost:7788/mcp",
|
|
273
|
+
"headers": {
|
|
274
|
+
"x-mcp-session-mode": "stateless"
|
|
275
|
+
}
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
278
|
}
|
|
276
279
|
```
|
|
277
280
|
|
|
278
|
-
CherryStudio
|
|
281
|
+
CherryStudio requires the `x-mcp-session-mode: stateless` header to use stateless mode. UA matching is NOT automatic — it requires manually adding `"CherryStudio"` to `system.session.sessionModeRules.stateless` in `.mcp-hub.json`. The header approach is the recommended way and takes highest priority.
|
|
282
|
+
|
|
283
|
+
Stateless mode uses per-request transport — each POST creates an independent transport, no session persistence, GET returns 405.
|
|
279
284
|
|
|
280
285
|
</details>
|
|
281
286
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-tools.d.ts","sourceRoot":"","sources":["../../../../../src/api/web/hub-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAqB1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"hub-tools.d.ts","sourceRoot":"","sources":["../../../../../src/api/web/hub-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAqB1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,eAAe,iBAsL/D"}
|
|
@@ -168,13 +168,13 @@ export async function webHubToolsRoutes(fastify) {
|
|
|
168
168
|
});
|
|
169
169
|
// GET /web/hub-tools/search - Search tools across all connected servers
|
|
170
170
|
fastify.get('/web/hub-tools/search', async (request, reply) => {
|
|
171
|
-
const { q } = request.query;
|
|
171
|
+
const { q, limit } = request.query;
|
|
172
172
|
if (!q || typeof q !== 'string' || q.trim().length === 0) {
|
|
173
173
|
return reply.code(400).send({
|
|
174
174
|
error: 'Bad Request',
|
|
175
175
|
message: 'Query parameter "q" is required'
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
|
-
return hubToolsService.searchTools(q.trim());
|
|
178
|
+
return hubToolsService.searchTools(q.trim(), limit);
|
|
179
179
|
});
|
|
180
180
|
}
|
|
@@ -4,7 +4,7 @@ import { FastifyInstance } from 'fastify';
|
|
|
4
4
|
*
|
|
5
5
|
* Returns aggregated tools from the gateway cache with wrapped inputSchema
|
|
6
6
|
* (including serverName, toolName, toolArgs, and requestOptions fields).
|
|
7
|
-
* Uses
|
|
7
|
+
* Uses tokenized matching on tool name and description, sorted by match count.
|
|
8
8
|
*
|
|
9
9
|
* @param fastify - The Fastify instance to register routes on
|
|
10
10
|
* @returns Promise that resolves when all routes are registered
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../../src/api/web/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../../../src/api/web/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,iBAoD7D"}
|
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
import { MCP_HUB_LITE_SERVER } from '../../../shared/models/constants.js';
|
|
2
|
+
import { countMatchingTokens } from '../../utils/search-matcher.js';
|
|
2
3
|
/**
|
|
3
4
|
* Tool Search API Routes
|
|
4
5
|
*
|
|
5
6
|
* Returns aggregated tools from the gateway cache with wrapped inputSchema
|
|
6
7
|
* (including serverName, toolName, toolArgs, and requestOptions fields).
|
|
7
|
-
* Uses
|
|
8
|
+
* Uses tokenized matching on tool name and description, sorted by match count.
|
|
8
9
|
*
|
|
9
10
|
* @param fastify - The Fastify instance to register routes on
|
|
10
11
|
* @returns Promise that resolves when all routes are registered
|
|
11
12
|
*/
|
|
12
13
|
export async function webSearchRoutes(fastify) {
|
|
13
|
-
// GET /web/search - Search for tools with
|
|
14
|
+
// GET /web/search - Search for tools with tokenized matching
|
|
14
15
|
fastify.get('/web/search', async (request) => {
|
|
15
|
-
const { q, limit =
|
|
16
|
+
const { q, limit = 5 } = request.query;
|
|
17
|
+
const effectiveLimit = Math.min(Math.max(1, limit), 10);
|
|
16
18
|
// Dynamic import to avoid circular dependency at module init time
|
|
17
19
|
const { getExternalGatewayTools } = await import('../../services/gateway/tool-list-generator.js');
|
|
18
20
|
const gatewayTools = getExternalGatewayTools();
|
|
19
21
|
const query = q?.toLowerCase() || '';
|
|
20
|
-
//
|
|
21
|
-
const
|
|
22
|
+
// Score, filter, sort, and slice by token match count
|
|
23
|
+
const scored = gatewayTools
|
|
24
|
+
.map((tool) => {
|
|
22
25
|
if (!query)
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
26
|
+
return { tool, matchCount: Number.MAX_SAFE_INTEGER };
|
|
27
|
+
const matchCount = countMatchingTokens(query, [tool.name, tool.description || '']);
|
|
28
|
+
return { tool, matchCount };
|
|
29
|
+
})
|
|
30
|
+
.filter((item) => item.matchCount > 0)
|
|
31
|
+
.sort((a, b) => b.matchCount - a.matchCount)
|
|
32
|
+
.slice(0, effectiveLimit);
|
|
33
|
+
const results = scored.map(({ tool }) => {
|
|
31
34
|
// Extract serverName from description format "[From serverName] ..."
|
|
32
35
|
const descMatch = tool.description?.match(/^\[From\s+(.+?)\]/);
|
|
33
36
|
const serverName = descMatch ? descMatch[1] : MCP_HUB_LITE_SERVER;
|
|
@@ -38,12 +41,11 @@ export async function webSearchRoutes(fastify) {
|
|
|
38
41
|
inputSchema: tool.inputSchema
|
|
39
42
|
};
|
|
40
43
|
});
|
|
41
|
-
const results = mappedResults.slice(0, limit);
|
|
42
44
|
return {
|
|
43
45
|
results,
|
|
44
46
|
pagination: {
|
|
45
|
-
total:
|
|
46
|
-
limit,
|
|
47
|
+
total: results.length,
|
|
48
|
+
limit: effectiveLimit,
|
|
47
49
|
returned: results.length
|
|
48
50
|
},
|
|
49
51
|
metadata: {
|
|
@@ -37,6 +37,7 @@ export interface ListTagsParams {
|
|
|
37
37
|
}
|
|
38
38
|
export interface SearchToolsParams {
|
|
39
39
|
query: string;
|
|
40
|
+
limit?: number;
|
|
40
41
|
}
|
|
41
42
|
export type SystemToolArgs = ListServersParams | ListToolsInServerParams | GetToolParams | CallToolParams | UpdateServerDescriptionParams | ListTagsParams | SearchToolsParams;
|
|
42
43
|
export declare const LIST_SERVERS_TOOL = "list_servers";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-tools.constants.d.ts","sourceRoot":"","sources":["../../../../src/models/system-tools.constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"system-tools.constants.d.ts","sourceRoot":"","sources":["../../../../src/models/system-tools.constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAGD,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,uBAAuB,GACvB,aAAa,GACb,cAAc,GACd,6BAA6B,GAC7B,cAAc,GACd,iBAAiB,CAAC;AAGtB,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAChD,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAC1E,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,4HAQpB,CAAC;AAEX;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAGhE;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-tools-handler.d.ts","sourceRoot":"","sources":["../../../../../../src/services/gateway/request-handlers/system-tools-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"system-tools-handler.d.ts","sourceRoot":"","sources":["../../../../../../src/services/gateway/request-handlers/system-tools-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA8OnE"}
|
|
@@ -223,14 +223,15 @@ export function registerSystemToolsHandlers(server) {
|
|
|
223
223
|
const SearchToolsRequestSchema = z.object({
|
|
224
224
|
method: z.literal(SEARCH_TOOLS_TOOL),
|
|
225
225
|
params: z.object({
|
|
226
|
-
query: z.string()
|
|
226
|
+
query: z.string(),
|
|
227
|
+
limit: z.number().int().min(1).max(10).optional()
|
|
227
228
|
}),
|
|
228
229
|
id: z.union([z.string(), z.number()]),
|
|
229
230
|
jsonrpc: z.literal('2.0')
|
|
230
231
|
});
|
|
231
232
|
server.server.setRequestHandler(SearchToolsRequestSchema, async (request) => {
|
|
232
233
|
try {
|
|
233
|
-
const result = await hubToolsService.searchTools(request.params.query);
|
|
234
|
+
const result = await hubToolsService.searchTools(request.params.query, request.params.limit);
|
|
234
235
|
return result;
|
|
235
236
|
}
|
|
236
237
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-tool-definitions.d.ts","sourceRoot":"","sources":["../../../../../src/services/hub-tools/system-tool-definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAY/D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,EAAE,
|
|
1
|
+
{"version":3,"file":"system-tool-definitions.d.ts","sourceRoot":"","sources":["../../../../../src/services/hub-tools/system-tool-definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAY/D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,EAAE,CA+LvD"}
|
|
@@ -185,6 +185,13 @@ export function getSystemTools() {
|
|
|
185
185
|
query: {
|
|
186
186
|
type: 'string',
|
|
187
187
|
description: 'Search query to match against tool names and descriptions'
|
|
188
|
+
},
|
|
189
|
+
limit: {
|
|
190
|
+
type: 'integer',
|
|
191
|
+
description: 'Maximum number of results to return per server (1-10, default 5)',
|
|
192
|
+
minimum: 1,
|
|
193
|
+
maximum: 10,
|
|
194
|
+
default: 5
|
|
188
195
|
}
|
|
189
196
|
},
|
|
190
197
|
required: ['query']
|
|
@@ -202,7 +202,7 @@ export declare class HubToolsService {
|
|
|
202
202
|
* @returns {Promise<Record<string, { description: string; tools: ToolSummary[] }>>}
|
|
203
203
|
* Object mapping server names to their descriptions and matching tools
|
|
204
204
|
*/
|
|
205
|
-
searchTools(query: string): Promise<Record<string, {
|
|
205
|
+
searchTools(query: string, limit?: number): Promise<Record<string, {
|
|
206
206
|
description: string;
|
|
207
207
|
tools: ToolSummary[];
|
|
208
208
|
}>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-tools.service.d.ts","sourceRoot":"","sources":["../../../../src/services/hub-tools.service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"hub-tools.service.d.ts","sourceRoot":"","sources":["../../../../src/services/hub-tools.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import { generateGatewayToolsList } from './gateway/tool-list-generator.js';
|
|
|
6
6
|
import { logger, LOG_MODULES } from '../utils/logger/index.js';
|
|
7
7
|
import { stringifyForLogging } from '../utils/json-utils.js';
|
|
8
8
|
import { normalizeToolName } from '../utils/name-converter.js';
|
|
9
|
+
import { countMatchingTokens } from '../utils/search-matcher.js';
|
|
9
10
|
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
10
11
|
import { MCP_HUB_LITE_SERVER, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL, SEARCH_TOOLS_TOOL, SYSTEM_TOOL_NAMES } from '../models/system-tools.constants.js';
|
|
11
12
|
import { ToolArgsParser } from '../utils/tool-args-parser.js';
|
|
@@ -601,11 +602,11 @@ export class HubToolsService {
|
|
|
601
602
|
* @returns {Promise<Record<string, { description: string; tools: ToolSummary[] }>>}
|
|
602
603
|
* Object mapping server names to their descriptions and matching tools
|
|
603
604
|
*/
|
|
604
|
-
async searchTools(query) {
|
|
605
|
+
async searchTools(query, limit = 5) {
|
|
605
606
|
if (!query || typeof query !== 'string') {
|
|
606
607
|
throw new Error('query is required and must be a non-empty string');
|
|
607
608
|
}
|
|
608
|
-
const
|
|
609
|
+
const effectiveLimit = Math.min(Math.max(1, limit), 10);
|
|
609
610
|
const servers = hubManager.getAllServers();
|
|
610
611
|
const result = {};
|
|
611
612
|
for (const server of servers) {
|
|
@@ -621,21 +622,26 @@ export class HubToolsService {
|
|
|
621
622
|
if (tools.length === 0) {
|
|
622
623
|
continue;
|
|
623
624
|
}
|
|
624
|
-
const
|
|
625
|
-
.
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
625
|
+
const scored = tools
|
|
626
|
+
.map((tool) => {
|
|
627
|
+
const matchCount = countMatchingTokens(query, [tool.name, tool.description || '']);
|
|
628
|
+
return {
|
|
629
|
+
tool,
|
|
630
|
+
matchCount,
|
|
631
|
+
summary: {
|
|
632
|
+
name: tool.name,
|
|
633
|
+
description: tool.description,
|
|
634
|
+
serverName: server.name
|
|
635
|
+
}
|
|
636
|
+
};
|
|
629
637
|
})
|
|
630
|
-
.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}));
|
|
635
|
-
if (matchingTools.length > 0) {
|
|
638
|
+
.filter((item) => item.matchCount > 0)
|
|
639
|
+
.sort((a, b) => b.matchCount - a.matchCount)
|
|
640
|
+
.slice(0, effectiveLimit);
|
|
641
|
+
if (scored.length > 0) {
|
|
636
642
|
result[server.name] = {
|
|
637
643
|
description,
|
|
638
|
-
tools:
|
|
644
|
+
tools: scored.map((item) => item.summary)
|
|
639
645
|
};
|
|
640
646
|
}
|
|
641
647
|
}
|
|
@@ -63,7 +63,7 @@ export class SystemToolHandler {
|
|
|
63
63
|
if (!searchArgs.query) {
|
|
64
64
|
throw new McpError(-32802, 'query is required for search_tools');
|
|
65
65
|
}
|
|
66
|
-
result = await hubToolsService.searchTools(searchArgs.query);
|
|
66
|
+
result = await hubToolsService.searchTools(searchArgs.query, searchArgs.limit);
|
|
67
67
|
break;
|
|
68
68
|
}
|
|
69
69
|
default:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts how many tokens from a whitespace-delimited query match against any of the given fields.
|
|
3
|
+
* Returns a large value for empty or whitespace-only queries so they match all results.
|
|
4
|
+
*/
|
|
5
|
+
export declare function countMatchingTokens(query: string, fields: string[]): number;
|
|
6
|
+
//# sourceMappingURL=search-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-matcher.d.ts","sourceRoot":"","sources":["../../../../src/utils/search-matcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAwB3E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts how many tokens from a whitespace-delimited query match against any of the given fields.
|
|
3
|
+
* Returns a large value for empty or whitespace-only queries so they match all results.
|
|
4
|
+
*/
|
|
5
|
+
export function countMatchingTokens(query, fields) {
|
|
6
|
+
if (!query || typeof query !== 'string') {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
const tokens = query
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.split(/[,\s;|]+/)
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (tokens.length === 0) {
|
|
14
|
+
return Number.MAX_SAFE_INTEGER;
|
|
15
|
+
}
|
|
16
|
+
const lowerFields = fields.map((f) => (f || '').toLowerCase());
|
|
17
|
+
let matchCount = 0;
|
|
18
|
+
for (const token of tokens) {
|
|
19
|
+
if (lowerFields.some((field) => field.includes(token))) {
|
|
20
|
+
matchCount++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return matchCount;
|
|
24
|
+
}
|
|
@@ -698,6 +698,301 @@ describe('HubToolsService', () => {
|
|
|
698
698
|
expect(allTools['Server 2'].tools).toEqual(expectedToolSummariesServer2);
|
|
699
699
|
});
|
|
700
700
|
});
|
|
701
|
+
describe('searchTools', () => {
|
|
702
|
+
it('should return tools matching a single-word query', async () => {
|
|
703
|
+
const mockServers = [
|
|
704
|
+
{
|
|
705
|
+
name: 'Server 1',
|
|
706
|
+
config: {
|
|
707
|
+
template: {
|
|
708
|
+
type: 'stdio',
|
|
709
|
+
command: 'test',
|
|
710
|
+
args: [],
|
|
711
|
+
env: {},
|
|
712
|
+
headers: {},
|
|
713
|
+
aggregatedTools: [],
|
|
714
|
+
timeout: 30000,
|
|
715
|
+
tags: {}
|
|
716
|
+
},
|
|
717
|
+
instances: [
|
|
718
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
719
|
+
],
|
|
720
|
+
tagDefinitions: []
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
];
|
|
724
|
+
const mockTools = [
|
|
725
|
+
{ name: 'readFile', description: 'Read file contents', serverName: 'Server 1' },
|
|
726
|
+
{ name: 'writeFile', description: 'Write file contents', serverName: 'Server 1' },
|
|
727
|
+
{ name: 'deleteFile', description: 'Delete files', serverName: 'Server 1' }
|
|
728
|
+
];
|
|
729
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
730
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
731
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
732
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
733
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
734
|
+
const result = await hubToolsService.searchTools('file');
|
|
735
|
+
expect(result).toHaveProperty('Server 1');
|
|
736
|
+
expect(result['Server 1'].tools).toHaveLength(3);
|
|
737
|
+
});
|
|
738
|
+
it('should match multi-word query by tokenizing and using OR logic', async () => {
|
|
739
|
+
const mockServers = [
|
|
740
|
+
{
|
|
741
|
+
name: 'Server 1',
|
|
742
|
+
config: {
|
|
743
|
+
template: {
|
|
744
|
+
type: 'stdio',
|
|
745
|
+
command: 'test',
|
|
746
|
+
args: [],
|
|
747
|
+
env: {},
|
|
748
|
+
headers: {},
|
|
749
|
+
aggregatedTools: [],
|
|
750
|
+
timeout: 30000,
|
|
751
|
+
tags: {}
|
|
752
|
+
},
|
|
753
|
+
instances: [
|
|
754
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
755
|
+
],
|
|
756
|
+
tagDefinitions: []
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
];
|
|
760
|
+
const mockTools = [
|
|
761
|
+
{ name: 'readFile', description: 'Read file contents', serverName: 'Server 1' },
|
|
762
|
+
{ name: 'getEnv', description: 'Get environment variables', serverName: 'Server 1' },
|
|
763
|
+
{ name: 'deleteFile', description: 'Delete files', serverName: 'Server 1' }
|
|
764
|
+
];
|
|
765
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
766
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
767
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
768
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
769
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
770
|
+
const result = await hubToolsService.searchTools('environment variable');
|
|
771
|
+
expect(result).toHaveProperty('Server 1');
|
|
772
|
+
// "getEnv" has "environment" in description but "variable" is NOT in "environment" (we need "variables")
|
|
773
|
+
// Actually: "environment" is in "environment variables", and "variable" is NOT in "environment variables"
|
|
774
|
+
// But "variable" is a substring of "variables", so it matches!
|
|
775
|
+
// Both tokens match getEnv, and only "file" matches readFile (but not "environment" or "variable")
|
|
776
|
+
// Wait: readFile has "file" in name and "Read file contents" in description — no "environment" or "variable" match
|
|
777
|
+
const toolNames = result['Server 1'].tools.map((t) => t.name);
|
|
778
|
+
expect(toolNames).toContain('getEnv');
|
|
779
|
+
});
|
|
780
|
+
it('should sort results by match count descending', async () => {
|
|
781
|
+
const mockServers = [
|
|
782
|
+
{
|
|
783
|
+
name: 'Server 1',
|
|
784
|
+
config: {
|
|
785
|
+
template: {
|
|
786
|
+
type: 'stdio',
|
|
787
|
+
command: 'test',
|
|
788
|
+
args: [],
|
|
789
|
+
env: {},
|
|
790
|
+
headers: {},
|
|
791
|
+
aggregatedTools: [],
|
|
792
|
+
timeout: 30000,
|
|
793
|
+
tags: {}
|
|
794
|
+
},
|
|
795
|
+
instances: [
|
|
796
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
797
|
+
],
|
|
798
|
+
tagDefinitions: []
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
];
|
|
802
|
+
const mockTools = [
|
|
803
|
+
{ name: 'envSetter', description: 'Set environment values', serverName: 'Server 1' },
|
|
804
|
+
{ name: 'getEnv', description: 'Get environment variables', serverName: 'Server 1' },
|
|
805
|
+
{ name: 'deleteFile', description: 'Delete environment files', serverName: 'Server 1' }
|
|
806
|
+
];
|
|
807
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
808
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
809
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
810
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
811
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
812
|
+
const result = await hubToolsService.searchTools('environment variable');
|
|
813
|
+
expect(result).toHaveProperty('Server 1');
|
|
814
|
+
const toolNames = result['Server 1'].tools.map((t) => t.name);
|
|
815
|
+
// All have "environment" in description, but match count varies:
|
|
816
|
+
// getEnv: desc "Get environment variables" — tokens "environment" matches, "variable" matches "variables" → 2
|
|
817
|
+
// envSetter: desc "Set environment values" — "environment" matches, "variable" matches... "values"? No, "variable" ≠ "values". So only 1 match.
|
|
818
|
+
// deleteFile: desc "Delete environment files" — "environment" matches, "variable"? No. Only 1 match.
|
|
819
|
+
// But both have 1 match. Sort order between them is stable but unspecified.
|
|
820
|
+
expect(toolNames[0]).toBe('getEnv'); // 2 matches, should be first
|
|
821
|
+
});
|
|
822
|
+
it('should apply default limit of 5', async () => {
|
|
823
|
+
const mockServers = [
|
|
824
|
+
{
|
|
825
|
+
name: 'Server 1',
|
|
826
|
+
config: {
|
|
827
|
+
template: {
|
|
828
|
+
type: 'stdio',
|
|
829
|
+
command: 'test',
|
|
830
|
+
args: [],
|
|
831
|
+
env: {},
|
|
832
|
+
headers: {},
|
|
833
|
+
aggregatedTools: [],
|
|
834
|
+
timeout: 30000,
|
|
835
|
+
tags: {}
|
|
836
|
+
},
|
|
837
|
+
instances: [
|
|
838
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
839
|
+
],
|
|
840
|
+
tagDefinitions: []
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
];
|
|
844
|
+
const mockTools = Array.from({ length: 10 }, (_, i) => ({
|
|
845
|
+
name: `tool${i}`,
|
|
846
|
+
description: 'A file handling tool',
|
|
847
|
+
serverName: 'Server 1'
|
|
848
|
+
}));
|
|
849
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
850
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
851
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
852
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
853
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
854
|
+
const result = await hubToolsService.searchTools('file');
|
|
855
|
+
expect(result).toHaveProperty('Server 1');
|
|
856
|
+
expect(result['Server 1'].tools.length).toBeLessThanOrEqual(5);
|
|
857
|
+
});
|
|
858
|
+
it('should respect custom limit parameter', async () => {
|
|
859
|
+
const mockServers = [
|
|
860
|
+
{
|
|
861
|
+
name: 'Server 1',
|
|
862
|
+
config: {
|
|
863
|
+
template: {
|
|
864
|
+
type: 'stdio',
|
|
865
|
+
command: 'test',
|
|
866
|
+
args: [],
|
|
867
|
+
env: {},
|
|
868
|
+
headers: {},
|
|
869
|
+
aggregatedTools: [],
|
|
870
|
+
timeout: 30000,
|
|
871
|
+
tags: {}
|
|
872
|
+
},
|
|
873
|
+
instances: [
|
|
874
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
875
|
+
],
|
|
876
|
+
tagDefinitions: []
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
];
|
|
880
|
+
const mockTools = Array.from({ length: 10 }, (_, i) => ({
|
|
881
|
+
name: `tool${i}`,
|
|
882
|
+
description: 'A file handling tool',
|
|
883
|
+
serverName: 'Server 1'
|
|
884
|
+
}));
|
|
885
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
886
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
887
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
888
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
889
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
890
|
+
const result = await hubToolsService.searchTools('file', 3);
|
|
891
|
+
expect(result).toHaveProperty('Server 1');
|
|
892
|
+
expect(result['Server 1'].tools.length).toBeLessThanOrEqual(3);
|
|
893
|
+
});
|
|
894
|
+
it('should return empty result when no tools match', async () => {
|
|
895
|
+
const mockServers = [
|
|
896
|
+
{
|
|
897
|
+
name: 'Server 1',
|
|
898
|
+
config: {
|
|
899
|
+
template: {
|
|
900
|
+
type: 'stdio',
|
|
901
|
+
command: 'test',
|
|
902
|
+
args: [],
|
|
903
|
+
env: {},
|
|
904
|
+
headers: {},
|
|
905
|
+
aggregatedTools: [],
|
|
906
|
+
timeout: 30000,
|
|
907
|
+
tags: {}
|
|
908
|
+
},
|
|
909
|
+
instances: [
|
|
910
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
911
|
+
],
|
|
912
|
+
tagDefinitions: []
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
];
|
|
916
|
+
const mockTools = [
|
|
917
|
+
{ name: 'readFile', description: 'Read file contents', serverName: 'Server 1' }
|
|
918
|
+
];
|
|
919
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
920
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
921
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
922
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
923
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
924
|
+
const result = await hubToolsService.searchTools('zzznotfound');
|
|
925
|
+
expect(result).toEqual({});
|
|
926
|
+
});
|
|
927
|
+
it('should handle extra whitespace in query', async () => {
|
|
928
|
+
const mockServers = [
|
|
929
|
+
{
|
|
930
|
+
name: 'Server 1',
|
|
931
|
+
config: {
|
|
932
|
+
template: {
|
|
933
|
+
type: 'stdio',
|
|
934
|
+
command: 'test',
|
|
935
|
+
args: [],
|
|
936
|
+
env: {},
|
|
937
|
+
headers: {},
|
|
938
|
+
aggregatedTools: [],
|
|
939
|
+
timeout: 30000,
|
|
940
|
+
tags: {}
|
|
941
|
+
},
|
|
942
|
+
instances: [
|
|
943
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
944
|
+
],
|
|
945
|
+
tagDefinitions: []
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
];
|
|
949
|
+
const mockTools = [
|
|
950
|
+
{ name: 'readFile', description: 'Read file contents', serverName: 'Server 1' }
|
|
951
|
+
];
|
|
952
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
953
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
954
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
955
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
956
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
957
|
+
const result = await hubToolsService.searchTools(' read file ');
|
|
958
|
+
expect(result).toHaveProperty('Server 1');
|
|
959
|
+
});
|
|
960
|
+
it('should cap limit at 10', async () => {
|
|
961
|
+
const mockServers = [
|
|
962
|
+
{
|
|
963
|
+
name: 'Server 1',
|
|
964
|
+
config: {
|
|
965
|
+
template: {
|
|
966
|
+
type: 'stdio',
|
|
967
|
+
command: 'test',
|
|
968
|
+
args: [],
|
|
969
|
+
env: {},
|
|
970
|
+
headers: {},
|
|
971
|
+
aggregatedTools: [],
|
|
972
|
+
timeout: 30000,
|
|
973
|
+
tags: {}
|
|
974
|
+
},
|
|
975
|
+
instances: [
|
|
976
|
+
{ id: '1', index: 0, enabled: true, args: [], env: {}, headers: {}, tags: {} }
|
|
977
|
+
],
|
|
978
|
+
tagDefinitions: []
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
];
|
|
982
|
+
const mockTools = Array.from({ length: 15 }, (_, i) => ({
|
|
983
|
+
name: `tool${i}`,
|
|
984
|
+
description: 'file tool',
|
|
985
|
+
serverName: 'Server 1'
|
|
986
|
+
}));
|
|
987
|
+
vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
|
|
988
|
+
vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
|
|
989
|
+
vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
|
|
990
|
+
vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
|
|
991
|
+
vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
|
|
992
|
+
const result = await hubToolsService.searchTools('file', 100);
|
|
993
|
+
expect(result['Server 1'].tools.length).toBeLessThanOrEqual(10);
|
|
994
|
+
});
|
|
995
|
+
});
|
|
701
996
|
describe('listResources', () => {
|
|
702
997
|
it('should return use-guide resource even when no servers are connected', async () => {
|
|
703
998
|
// Arrange
|