@loop_ouroboros/mcp-hub-lite 1.3.0 → 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 +59 -0
- package/README.md +410 -331
- package/dist/client/assets/{HomeView-Bi2bkUKf.js → HomeView-DplI3V-h.js} +1 -1
- package/dist/client/assets/{ResourceDetailView-DyuSovH9.js → ResourceDetailView-CeHPn99Y.js} +1 -1
- package/dist/client/assets/ResourcesView-C1ObRhYS.js +1 -0
- package/dist/client/assets/{ServerDashboard-BGyyZAti.js → ServerDashboard-D7wG4Gvt.js} +1 -1
- package/dist/client/assets/ServerDetail-G23phOcJ.js +2 -0
- package/dist/client/assets/{ServerListView-yQPVJFHG.js → ServerListView-BFiZLtPO.js} +1 -1
- package/dist/client/assets/{ServerStatusTags.vue_vue_type_script_setup_true_lang-C8gQlxGE.js → ServerStatusTags.vue_vue_type_script_setup_true_lang-Deb_SbFw.js} +1 -1
- package/dist/client/assets/SettingsView-QBFLZ6fP.js +1 -0
- package/dist/client/assets/ToolCallDialog-DYS-ADCL.js +1 -0
- package/dist/client/assets/ToolsView-DYwgtm7W.js +1 -0
- package/dist/client/assets/_baseClone-DQno9YO3.js +1 -0
- package/dist/client/assets/{el-form-item-DfWq_kSy.js → el-form-item-DF0zzQdH.js} +2 -2
- package/dist/client/assets/el-input-C_p2Qw42.js +1 -0
- package/dist/client/assets/el-loading-BaenpNzU.js +1 -0
- package/dist/client/assets/el-overlay-MbIUXSQ7.js +1 -0
- package/dist/client/assets/el-radio-group-COnCjCcz.js +1 -0
- package/dist/client/assets/el-skeleton-item-qj0eQP4s.js +1 -0
- package/dist/client/assets/el-switch-BZbXqB3_.js +1 -0
- package/dist/client/assets/el-tab-pane-w7RltRLd.js +1 -0
- package/dist/client/assets/el-table-column-OD8zhFcD.js +1 -0
- package/dist/client/assets/index-DwhULJXZ.js +2 -0
- package/dist/client/assets/{index-Bzz3tYbS.css → index-UtsV0Cvh.css} +1 -1
- package/dist/client/assets/{omit-BIIebEYo.js → omit-BAJQlviJ.js} +1 -1
- package/dist/client/assets/raf-B1Ry7ruA.js +1 -0
- package/dist/client/assets/{vue-vendor-Dwcr0jep.js → vue-vendor-ClSvefnQ.js} +1 -1
- package/dist/client/index.html +3 -3
- package/dist/server/shared/models/constants.d.ts +5 -0
- package/dist/server/shared/models/constants.d.ts.map +1 -1
- package/dist/server/shared/models/constants.js +4 -0
- package/dist/server/shared/models/server.model.d.ts +14 -0
- package/dist/server/shared/models/server.model.d.ts.map +1 -1
- package/dist/server/shared/models/server.model.js +27 -4
- package/dist/server/src/api/mcp/gateway.d.ts +10 -6
- package/dist/server/src/api/mcp/gateway.d.ts.map +1 -1
- package/dist/server/src/api/mcp/gateway.js +235 -69
- 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/api/web/sessions.d.ts +1 -27
- package/dist/server/src/api/web/sessions.d.ts.map +1 -1
- package/dist/server/src/api/web/sessions.js +8 -97
- package/dist/server/src/app.d.ts.map +1 -1
- package/dist/server/src/app.js +5 -0
- package/dist/server/src/cli/commands/status.js +39 -1
- package/dist/server/src/cli/commands/use-guide.d.ts +0 -8
- package/dist/server/src/cli/commands/use-guide.d.ts.map +1 -1
- package/dist/server/src/cli/commands/use-guide.js +28 -170
- package/dist/server/src/cli/server.d.ts +10 -0
- package/dist/server/src/cli/server.d.ts.map +1 -1
- package/dist/server/src/cli/server.js +31 -1
- 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/server/dev-server.js +2 -0
- package/dist/server/src/server/runner.d.ts.map +1 -1
- package/dist/server/src/server/runner.js +2 -0
- package/dist/server/src/services/connection/connection-manager.d.ts +2 -0
- package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
- package/dist/server/src/services/connection/connection-manager.js +14 -7
- package/dist/server/src/services/gateway/gateway.service.d.ts +13 -0
- package/dist/server/src/services/gateway/gateway.service.d.ts.map +1 -1
- package/dist/server/src/services/gateway/gateway.service.js +72 -0
- package/dist/server/src/services/gateway/global-transport.d.ts +20 -10
- package/dist/server/src/services/gateway/global-transport.d.ts.map +1 -1
- package/dist/server/src/services/gateway/global-transport.js +50 -34
- package/dist/server/src/services/gateway/request-handlers/initialize-handler.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/initialize-handler.js +22 -6
- package/dist/server/src/services/gateway/request-handlers/resources-handler.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/resources-handler.js +5 -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/gateway/session-manager.d.ts +101 -0
- package/dist/server/src/services/gateway/session-manager.d.ts.map +1 -0
- package/dist/server/src/services/gateway/session-manager.js +256 -0
- package/dist/server/src/services/hub-tools/resource-generator.d.ts +1 -1
- package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/resource-generator.js +11 -9
- 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 +23 -15
- package/dist/server/src/services/system-tool-handler.js +1 -1
- package/dist/server/src/utils/json-utils.d.ts +9 -0
- package/dist/server/src/utils/json-utils.d.ts.map +1 -1
- package/dist/server/src/utils/json-utils.js +19 -0
- package/dist/server/src/utils/logger/index.d.ts +1 -1
- package/dist/server/src/utils/logger/index.d.ts.map +1 -1
- package/dist/server/src/utils/logger/index.js +1 -1
- package/dist/server/src/utils/logger/log-context.d.ts +1 -0
- package/dist/server/src/utils/logger/log-context.d.ts.map +1 -1
- package/dist/server/src/utils/logger/log-formatter.d.ts.map +1 -1
- package/dist/server/src/utils/logger/log-formatter.js +25 -11
- package/dist/server/src/utils/logger/log-output.d.ts +17 -1
- package/dist/server/src/utils/logger/log-output.d.ts.map +1 -1
- package/dist/server/src/utils/logger/log-output.js +46 -40
- package/dist/server/src/utils/logger/logger.d.ts.map +1 -1
- package/dist/server/src/utils/logger/logger.js +18 -2
- package/dist/server/src/utils/request-context.d.ts +8 -70
- package/dist/server/src/utils/request-context.d.ts.map +1 -1
- package/dist/server/src/utils/request-context.js +11 -70
- 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/config/config.schema.test.js +2 -1
- package/dist/server/tests/unit/server/runner.test.js +14 -7
- package/dist/server/tests/unit/services/gateway-session-mode.test.d.ts +2 -0
- package/dist/server/tests/unit/services/gateway-session-mode.test.d.ts.map +1 -0
- package/dist/server/tests/unit/services/gateway-session-mode.test.js +174 -0
- package/dist/server/tests/unit/services/hub-tools.service.test.js +299 -4
- package/dist/server/tests/unit/utils/config.test.js +14 -7
- package/dist/server/tests/unit/utils/log-output.test.d.ts +2 -0
- package/dist/server/tests/unit/utils/log-output.test.d.ts.map +1 -0
- package/dist/server/tests/unit/utils/log-output.test.js +198 -0
- package/dist/server/vitest.config.d.ts.map +1 -1
- package/dist/server/vitest.config.js +0 -2
- package/package.json +1 -1
- package/dist/client/assets/ResourcesView-CU0VbNy5.js +0 -1
- package/dist/client/assets/ServerDetail-bcQ8BVXR.js +0 -2
- package/dist/client/assets/SettingsView-B1DxbFP3.js +0 -1
- package/dist/client/assets/ToolCallDialog-DEapCO06.js +0 -1
- package/dist/client/assets/ToolsView-DA0u_bCw.js +0 -1
- package/dist/client/assets/_baseClone-B991Lvrt.js +0 -1
- package/dist/client/assets/el-input-5YzZrwir.js +0 -1
- package/dist/client/assets/el-loading-DE3FcxNH.js +0 -1
- package/dist/client/assets/el-overlay-BTeTueuN.js +0 -1
- package/dist/client/assets/el-radio-group-Y1E2bxIW.js +0 -1
- package/dist/client/assets/el-skeleton-item-DhgR50Jx.js +0 -1
- package/dist/client/assets/el-switch-fF--nMSD.js +0 -1
- package/dist/client/assets/el-tab-pane-rvS_KTwP.js +0 -1
- package/dist/client/assets/el-table-column-B1O8mY47.js +0 -1
- package/dist/client/assets/index-DkqV9kH4.js +0 -2
- package/dist/client/assets/raf-Cj-gATZv.js +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SESSION_MODE_STATEFUL, SESSION_MODE_STATELESS } from '../../../shared/models/constants.js';
|
|
3
|
+
// Mock configManager before importing the module under test
|
|
4
|
+
const mockGetConfig = vi.fn();
|
|
5
|
+
vi.mock('@config/config-manager.js', () => ({
|
|
6
|
+
configManager: {
|
|
7
|
+
getConfig: () => mockGetConfig()
|
|
8
|
+
}
|
|
9
|
+
}));
|
|
10
|
+
// We need to import resolveSessionMode from gateway.ts
|
|
11
|
+
// It's exported from the module, but the module has side-effects (Fastify routes)
|
|
12
|
+
// Use dynamic import after mocks are set up
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
let resolveSessionMode;
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
mockGetConfig.mockReset();
|
|
18
|
+
const mod = await import('../../../src/api/mcp/gateway.js');
|
|
19
|
+
resolveSessionMode = mod.resolveSessionMode;
|
|
20
|
+
});
|
|
21
|
+
function makeRequest(headers = {}) {
|
|
22
|
+
return { headers };
|
|
23
|
+
}
|
|
24
|
+
describe('resolveSessionMode', () => {
|
|
25
|
+
describe('request header override (highest priority)', () => {
|
|
26
|
+
test('x-mcp-session-mode: stateless overrides UA match', () => {
|
|
27
|
+
mockGetConfig.mockReturnValue({
|
|
28
|
+
system: {
|
|
29
|
+
session: {
|
|
30
|
+
sessionModeRules: { stateful: ['claude-code'], stateless: [] },
|
|
31
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const request = makeRequest({
|
|
36
|
+
'x-mcp-session-mode': SESSION_MODE_STATELESS,
|
|
37
|
+
'user-agent': 'claude-code/2.1.140 (cli)'
|
|
38
|
+
});
|
|
39
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
40
|
+
});
|
|
41
|
+
test('x-mcp-session-mode: stateful overrides UA match', () => {
|
|
42
|
+
mockGetConfig.mockReturnValue({
|
|
43
|
+
system: {
|
|
44
|
+
session: {
|
|
45
|
+
sessionModeRules: { stateful: [], stateless: ['cherrystudio'] },
|
|
46
|
+
defaultSessionMode: SESSION_MODE_STATELESS
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const request = makeRequest({
|
|
51
|
+
'x-mcp-session-mode': SESSION_MODE_STATEFUL,
|
|
52
|
+
'user-agent': 'CherryStudio/1.9.7'
|
|
53
|
+
});
|
|
54
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('UA keyword matching', () => {
|
|
58
|
+
test('matches stateless pattern (case-insensitive)', () => {
|
|
59
|
+
mockGetConfig.mockReturnValue({
|
|
60
|
+
system: {
|
|
61
|
+
session: {
|
|
62
|
+
sessionModeRules: { stateful: [], stateless: ['cherrystudio'] },
|
|
63
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const request = makeRequest({
|
|
68
|
+
'user-agent': 'Mozilla/5.0 ... CherryStudio/1.9.7 Chrome/146.0.7680.188 Electron/41.2.1'
|
|
69
|
+
});
|
|
70
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
71
|
+
});
|
|
72
|
+
test('matches stateless pattern with different casing in UA', () => {
|
|
73
|
+
mockGetConfig.mockReturnValue({
|
|
74
|
+
system: {
|
|
75
|
+
session: {
|
|
76
|
+
sessionModeRules: { stateful: [], stateless: ['cherrystudio'] },
|
|
77
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const request = makeRequest({ 'user-agent': 'CHERRYSTUDIO/1.9.7' });
|
|
82
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
83
|
+
});
|
|
84
|
+
test('matches stateless pattern with different casing in rule', () => {
|
|
85
|
+
mockGetConfig.mockReturnValue({
|
|
86
|
+
system: {
|
|
87
|
+
session: {
|
|
88
|
+
sessionModeRules: { stateful: [], stateless: ['CherryStudio'] },
|
|
89
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const request = makeRequest({ 'user-agent': 'cherrystudio/1.9.7' });
|
|
94
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
95
|
+
});
|
|
96
|
+
test('matches stateful pattern', () => {
|
|
97
|
+
mockGetConfig.mockReturnValue({
|
|
98
|
+
system: {
|
|
99
|
+
session: {
|
|
100
|
+
sessionModeRules: { stateful: ['claude-code'], stateless: [] },
|
|
101
|
+
defaultSessionMode: SESSION_MODE_STATELESS
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const request = makeRequest({ 'user-agent': 'claude-code/2.1.140 (cli)' });
|
|
106
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
107
|
+
});
|
|
108
|
+
test('stateless rules checked before stateful (stateless wins on conflict)', () => {
|
|
109
|
+
mockGetConfig.mockReturnValue({
|
|
110
|
+
system: {
|
|
111
|
+
session: {
|
|
112
|
+
sessionModeRules: { stateful: ['claude'], stateless: ['claude-code'] },
|
|
113
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// "claude-code" matches both "claude" and "claude-code", but stateless checked first
|
|
118
|
+
const request = makeRequest({ 'user-agent': 'claude-code/2.1.140' });
|
|
119
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('default fallback', () => {
|
|
123
|
+
test('no matching UA falls back to defaultSessionMode', () => {
|
|
124
|
+
mockGetConfig.mockReturnValue({
|
|
125
|
+
system: {
|
|
126
|
+
session: {
|
|
127
|
+
sessionModeRules: { stateful: ['claude-code'], stateless: ['cherrystudio'] },
|
|
128
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const request = makeRequest({ 'user-agent': 'SomeUnknownClient/1.0' });
|
|
133
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
134
|
+
});
|
|
135
|
+
test('empty UA falls back to defaultSessionMode', () => {
|
|
136
|
+
mockGetConfig.mockReturnValue({
|
|
137
|
+
system: {
|
|
138
|
+
session: {
|
|
139
|
+
sessionModeRules: { stateful: ['claude-code'], stateless: [] },
|
|
140
|
+
defaultSessionMode: SESSION_MODE_STATEFUL
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
const request = makeRequest({});
|
|
145
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
146
|
+
});
|
|
147
|
+
test('no gateway config falls back to stateful (hardcoded default)', () => {
|
|
148
|
+
mockGetConfig.mockReturnValue({
|
|
149
|
+
system: {}
|
|
150
|
+
});
|
|
151
|
+
const request = makeRequest({ 'user-agent': 'SomeClient/1.0' });
|
|
152
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
153
|
+
});
|
|
154
|
+
test('no config at all falls back to stateful', () => {
|
|
155
|
+
mockGetConfig.mockReturnValue(null);
|
|
156
|
+
const request = makeRequest({ 'user-agent': 'SomeClient/1.0' });
|
|
157
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATEFUL);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('empty rules arrays', () => {
|
|
161
|
+
test('empty stateful and stateless arrays use default', () => {
|
|
162
|
+
mockGetConfig.mockReturnValue({
|
|
163
|
+
system: {
|
|
164
|
+
session: {
|
|
165
|
+
sessionModeRules: { stateful: [], stateless: [] },
|
|
166
|
+
defaultSessionMode: SESSION_MODE_STATELESS
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const request = makeRequest({ 'user-agent': 'SomeClient/1.0' });
|
|
171
|
+
expect(resolveSessionMode(request)).toBe(SESSION_MODE_STATELESS);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -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
|
|
@@ -776,10 +1071,10 @@ describe('HubToolsService', () => {
|
|
|
776
1071
|
const result = await hubToolsService.readResource('hub://use-guide');
|
|
777
1072
|
// Assert
|
|
778
1073
|
expect(typeof result).toBe('string');
|
|
779
|
-
expect(result).toContain('# MCP Hub Lite
|
|
780
|
-
expect(result).toContain('
|
|
781
|
-
expect(result).toContain('
|
|
782
|
-
expect(result).toContain('
|
|
1074
|
+
expect(result).toContain('# MCP Hub Lite');
|
|
1075
|
+
expect(result).toContain('快速上手');
|
|
1076
|
+
expect(result).toContain('渐进式发现工作流');
|
|
1077
|
+
expect(result).toContain('系统工具参考');
|
|
783
1078
|
});
|
|
784
1079
|
it('should throw error for invalid URI format', async () => {
|
|
785
1080
|
// Act & Assert
|
|
@@ -81,7 +81,8 @@ describe('ConfigManager', () => {
|
|
|
81
81
|
jsonPretty: true,
|
|
82
82
|
mcpCommDebug: false,
|
|
83
83
|
apiDebug: false,
|
|
84
|
-
gatewayDebug: false
|
|
84
|
+
gatewayDebug: false,
|
|
85
|
+
showTraceContext: true
|
|
85
86
|
}
|
|
86
87
|
},
|
|
87
88
|
security: {
|
|
@@ -151,7 +152,8 @@ describe('ConfigManager', () => {
|
|
|
151
152
|
jsonPretty: true,
|
|
152
153
|
mcpCommDebug: false,
|
|
153
154
|
apiDebug: false,
|
|
154
|
-
gatewayDebug: false
|
|
155
|
+
gatewayDebug: false,
|
|
156
|
+
showTraceContext: true
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
});
|
|
@@ -196,7 +198,8 @@ describe('ConfigManager', () => {
|
|
|
196
198
|
jsonPretty: true,
|
|
197
199
|
mcpCommDebug: false,
|
|
198
200
|
apiDebug: false,
|
|
199
|
-
gatewayDebug: false
|
|
201
|
+
gatewayDebug: false,
|
|
202
|
+
showTraceContext: true
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
205
|
});
|
|
@@ -239,7 +242,8 @@ describe('ConfigManager', () => {
|
|
|
239
242
|
jsonPretty: true,
|
|
240
243
|
mcpCommDebug: false,
|
|
241
244
|
apiDebug: false,
|
|
242
|
-
gatewayDebug: false
|
|
245
|
+
gatewayDebug: false,
|
|
246
|
+
showTraceContext: true
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
};
|
|
@@ -409,7 +413,8 @@ describe('ConfigManager', () => {
|
|
|
409
413
|
jsonPretty: true,
|
|
410
414
|
mcpCommDebug: false,
|
|
411
415
|
apiDebug: false,
|
|
412
|
-
gatewayDebug: false
|
|
416
|
+
gatewayDebug: false,
|
|
417
|
+
showTraceContext: true
|
|
413
418
|
}
|
|
414
419
|
},
|
|
415
420
|
security: {
|
|
@@ -463,7 +468,8 @@ describe('ConfigManager', () => {
|
|
|
463
468
|
jsonPretty: true,
|
|
464
469
|
mcpCommDebug: false,
|
|
465
470
|
apiDebug: false,
|
|
466
|
-
gatewayDebug: false
|
|
471
|
+
gatewayDebug: false,
|
|
472
|
+
showTraceContext: true
|
|
467
473
|
}
|
|
468
474
|
}
|
|
469
475
|
});
|
|
@@ -481,7 +487,8 @@ describe('ConfigManager', () => {
|
|
|
481
487
|
jsonPretty: true,
|
|
482
488
|
mcpCommDebug: false,
|
|
483
489
|
apiDebug: false,
|
|
484
|
-
gatewayDebug: false
|
|
490
|
+
gatewayDebug: false,
|
|
491
|
+
showTraceContext: true
|
|
485
492
|
}
|
|
486
493
|
},
|
|
487
494
|
security: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-output.test.d.ts","sourceRoot":"","sources":["../../../../../tests/unit/utils/log-output.test.ts"],"names":[],"mappings":""}
|