@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +410 -331
  3. package/dist/client/assets/{HomeView-Bi2bkUKf.js → HomeView-DplI3V-h.js} +1 -1
  4. package/dist/client/assets/{ResourceDetailView-DyuSovH9.js → ResourceDetailView-CeHPn99Y.js} +1 -1
  5. package/dist/client/assets/ResourcesView-C1ObRhYS.js +1 -0
  6. package/dist/client/assets/{ServerDashboard-BGyyZAti.js → ServerDashboard-D7wG4Gvt.js} +1 -1
  7. package/dist/client/assets/ServerDetail-G23phOcJ.js +2 -0
  8. package/dist/client/assets/{ServerListView-yQPVJFHG.js → ServerListView-BFiZLtPO.js} +1 -1
  9. 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
  10. package/dist/client/assets/SettingsView-QBFLZ6fP.js +1 -0
  11. package/dist/client/assets/ToolCallDialog-DYS-ADCL.js +1 -0
  12. package/dist/client/assets/ToolsView-DYwgtm7W.js +1 -0
  13. package/dist/client/assets/_baseClone-DQno9YO3.js +1 -0
  14. package/dist/client/assets/{el-form-item-DfWq_kSy.js → el-form-item-DF0zzQdH.js} +2 -2
  15. package/dist/client/assets/el-input-C_p2Qw42.js +1 -0
  16. package/dist/client/assets/el-loading-BaenpNzU.js +1 -0
  17. package/dist/client/assets/el-overlay-MbIUXSQ7.js +1 -0
  18. package/dist/client/assets/el-radio-group-COnCjCcz.js +1 -0
  19. package/dist/client/assets/el-skeleton-item-qj0eQP4s.js +1 -0
  20. package/dist/client/assets/el-switch-BZbXqB3_.js +1 -0
  21. package/dist/client/assets/el-tab-pane-w7RltRLd.js +1 -0
  22. package/dist/client/assets/el-table-column-OD8zhFcD.js +1 -0
  23. package/dist/client/assets/index-DwhULJXZ.js +2 -0
  24. package/dist/client/assets/{index-Bzz3tYbS.css → index-UtsV0Cvh.css} +1 -1
  25. package/dist/client/assets/{omit-BIIebEYo.js → omit-BAJQlviJ.js} +1 -1
  26. package/dist/client/assets/raf-B1Ry7ruA.js +1 -0
  27. package/dist/client/assets/{vue-vendor-Dwcr0jep.js → vue-vendor-ClSvefnQ.js} +1 -1
  28. package/dist/client/index.html +3 -3
  29. package/dist/server/shared/models/constants.d.ts +5 -0
  30. package/dist/server/shared/models/constants.d.ts.map +1 -1
  31. package/dist/server/shared/models/constants.js +4 -0
  32. package/dist/server/shared/models/server.model.d.ts +14 -0
  33. package/dist/server/shared/models/server.model.d.ts.map +1 -1
  34. package/dist/server/shared/models/server.model.js +27 -4
  35. package/dist/server/src/api/mcp/gateway.d.ts +10 -6
  36. package/dist/server/src/api/mcp/gateway.d.ts.map +1 -1
  37. package/dist/server/src/api/mcp/gateway.js +235 -69
  38. package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
  39. package/dist/server/src/api/web/hub-tools.js +2 -2
  40. package/dist/server/src/api/web/search.d.ts +1 -1
  41. package/dist/server/src/api/web/search.d.ts.map +1 -1
  42. package/dist/server/src/api/web/search.js +18 -16
  43. package/dist/server/src/api/web/sessions.d.ts +1 -27
  44. package/dist/server/src/api/web/sessions.d.ts.map +1 -1
  45. package/dist/server/src/api/web/sessions.js +8 -97
  46. package/dist/server/src/app.d.ts.map +1 -1
  47. package/dist/server/src/app.js +5 -0
  48. package/dist/server/src/cli/commands/status.js +39 -1
  49. package/dist/server/src/cli/commands/use-guide.d.ts +0 -8
  50. package/dist/server/src/cli/commands/use-guide.d.ts.map +1 -1
  51. package/dist/server/src/cli/commands/use-guide.js +28 -170
  52. package/dist/server/src/cli/server.d.ts +10 -0
  53. package/dist/server/src/cli/server.d.ts.map +1 -1
  54. package/dist/server/src/cli/server.js +31 -1
  55. package/dist/server/src/models/system-tools.constants.d.ts +1 -0
  56. package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
  57. package/dist/server/src/server/dev-server.js +2 -0
  58. package/dist/server/src/server/runner.d.ts.map +1 -1
  59. package/dist/server/src/server/runner.js +2 -0
  60. package/dist/server/src/services/connection/connection-manager.d.ts +2 -0
  61. package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
  62. package/dist/server/src/services/connection/connection-manager.js +14 -7
  63. package/dist/server/src/services/gateway/gateway.service.d.ts +13 -0
  64. package/dist/server/src/services/gateway/gateway.service.d.ts.map +1 -1
  65. package/dist/server/src/services/gateway/gateway.service.js +72 -0
  66. package/dist/server/src/services/gateway/global-transport.d.ts +20 -10
  67. package/dist/server/src/services/gateway/global-transport.d.ts.map +1 -1
  68. package/dist/server/src/services/gateway/global-transport.js +50 -34
  69. package/dist/server/src/services/gateway/request-handlers/initialize-handler.d.ts.map +1 -1
  70. package/dist/server/src/services/gateway/request-handlers/initialize-handler.js +22 -6
  71. package/dist/server/src/services/gateway/request-handlers/resources-handler.d.ts.map +1 -1
  72. package/dist/server/src/services/gateway/request-handlers/resources-handler.js +5 -1
  73. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
  74. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +3 -2
  75. package/dist/server/src/services/gateway/session-manager.d.ts +101 -0
  76. package/dist/server/src/services/gateway/session-manager.d.ts.map +1 -0
  77. package/dist/server/src/services/gateway/session-manager.js +256 -0
  78. package/dist/server/src/services/hub-tools/resource-generator.d.ts +1 -1
  79. package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
  80. package/dist/server/src/services/hub-tools/resource-generator.js +11 -9
  81. package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
  82. package/dist/server/src/services/hub-tools/system-tool-definitions.js +7 -0
  83. package/dist/server/src/services/hub-tools.service.d.ts +1 -1
  84. package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
  85. package/dist/server/src/services/hub-tools.service.js +23 -15
  86. package/dist/server/src/services/system-tool-handler.js +1 -1
  87. package/dist/server/src/utils/json-utils.d.ts +9 -0
  88. package/dist/server/src/utils/json-utils.d.ts.map +1 -1
  89. package/dist/server/src/utils/json-utils.js +19 -0
  90. package/dist/server/src/utils/logger/index.d.ts +1 -1
  91. package/dist/server/src/utils/logger/index.d.ts.map +1 -1
  92. package/dist/server/src/utils/logger/index.js +1 -1
  93. package/dist/server/src/utils/logger/log-context.d.ts +1 -0
  94. package/dist/server/src/utils/logger/log-context.d.ts.map +1 -1
  95. package/dist/server/src/utils/logger/log-formatter.d.ts.map +1 -1
  96. package/dist/server/src/utils/logger/log-formatter.js +25 -11
  97. package/dist/server/src/utils/logger/log-output.d.ts +17 -1
  98. package/dist/server/src/utils/logger/log-output.d.ts.map +1 -1
  99. package/dist/server/src/utils/logger/log-output.js +46 -40
  100. package/dist/server/src/utils/logger/logger.d.ts.map +1 -1
  101. package/dist/server/src/utils/logger/logger.js +18 -2
  102. package/dist/server/src/utils/request-context.d.ts +8 -70
  103. package/dist/server/src/utils/request-context.d.ts.map +1 -1
  104. package/dist/server/src/utils/request-context.js +11 -70
  105. package/dist/server/src/utils/search-matcher.d.ts +6 -0
  106. package/dist/server/src/utils/search-matcher.d.ts.map +1 -0
  107. package/dist/server/src/utils/search-matcher.js +24 -0
  108. package/dist/server/tests/unit/config/config.schema.test.js +2 -1
  109. package/dist/server/tests/unit/server/runner.test.js +14 -7
  110. package/dist/server/tests/unit/services/gateway-session-mode.test.d.ts +2 -0
  111. package/dist/server/tests/unit/services/gateway-session-mode.test.d.ts.map +1 -0
  112. package/dist/server/tests/unit/services/gateway-session-mode.test.js +174 -0
  113. package/dist/server/tests/unit/services/hub-tools.service.test.js +299 -4
  114. package/dist/server/tests/unit/utils/config.test.js +14 -7
  115. package/dist/server/tests/unit/utils/log-output.test.d.ts +2 -0
  116. package/dist/server/tests/unit/utils/log-output.test.d.ts.map +1 -0
  117. package/dist/server/tests/unit/utils/log-output.test.js +198 -0
  118. package/dist/server/vitest.config.d.ts.map +1 -1
  119. package/dist/server/vitest.config.js +0 -2
  120. package/package.json +1 -1
  121. package/dist/client/assets/ResourcesView-CU0VbNy5.js +0 -1
  122. package/dist/client/assets/ServerDetail-bcQ8BVXR.js +0 -2
  123. package/dist/client/assets/SettingsView-B1DxbFP3.js +0 -1
  124. package/dist/client/assets/ToolCallDialog-DEapCO06.js +0 -1
  125. package/dist/client/assets/ToolsView-DA0u_bCw.js +0 -1
  126. package/dist/client/assets/_baseClone-B991Lvrt.js +0 -1
  127. package/dist/client/assets/el-input-5YzZrwir.js +0 -1
  128. package/dist/client/assets/el-loading-DE3FcxNH.js +0 -1
  129. package/dist/client/assets/el-overlay-BTeTueuN.js +0 -1
  130. package/dist/client/assets/el-radio-group-Y1E2bxIW.js +0 -1
  131. package/dist/client/assets/el-skeleton-item-DhgR50Jx.js +0 -1
  132. package/dist/client/assets/el-switch-fF--nMSD.js +0 -1
  133. package/dist/client/assets/el-tab-pane-rvS_KTwP.js +0 -1
  134. package/dist/client/assets/el-table-column-B1O8mY47.js +0 -1
  135. package/dist/client/assets/index-DkqV9kH4.js +0 -2
  136. 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 Use Guide');
780
- expect(result).toContain('Getting Started');
781
- expect(result).toContain('Progressive Discovery Workflow');
782
- expect(result).toContain('System Tools Reference');
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=log-output.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-output.test.d.ts","sourceRoot":"","sources":["../../../../../tests/unit/utils/log-output.test.ts"],"names":[],"mappings":""}