@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 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. No extra headers needed — Claude Code is detected automatically by UA matching.
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 is detected automatically by UA matching and routed to stateless mode. Stateless mode uses per-request transporteach POST creates an independent transport, no session persistence, GET returns 405.
281
+ CherryStudio requires the `x-mcp-session-mode: stateless` header to use stateless mode. UA matching is NOT automaticit 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,iBAmL/D"}
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 straightforward string matching on tool name and description.
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;AAG1C;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,iBAoD7D"}
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 straightforward string matching on tool name and description.
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 simple string matching
14
+ // GET /web/search - Search for tools with tokenized matching
14
15
  fastify.get('/web/search', async (request) => {
15
- const { q, limit = 50 } = request.query;
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
- // Filter by search query
21
- const queryMatched = gatewayTools.filter((tool) => {
22
+ // Score, filter, sort, and slice by token match count
23
+ const scored = gatewayTools
24
+ .map((tool) => {
22
25
  if (!query)
23
- return true;
24
- // Match against tool name (gateway-resolved name)
25
- const nameMatch = tool.name.toLowerCase().includes(query);
26
- // Match against description (contains "[From serverName] original description")
27
- const descMatch = tool.description?.toLowerCase().includes(query);
28
- return nameMatch || descMatch;
29
- });
30
- const mappedResults = queryMatched.map((tool) => {
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: mappedResults.length,
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;CACf;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
+ {"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,CA6OnE"}
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,CAwLvD"}
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":"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"}
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 normalizedQuery = query.toLowerCase();
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 matchingTools = tools
625
- .filter((tool) => {
626
- const nameMatch = tool.name.toLowerCase().includes(normalizedQuery);
627
- const descMatch = tool.description?.toLowerCase().includes(normalizedQuery);
628
- return nameMatch || descMatch;
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
- .map((tool) => ({
631
- name: tool.name,
632
- description: tool.description,
633
- serverName: server.name
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: matchingTools
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loop_ouroboros/mcp-hub-lite",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "A lightweight MCP management platform designed for independent developers",
5
5
  "license": "MIT",
6
6
  "author": "loop_ouroboros",