@loop_ouroboros/mcp-hub-lite 1.2.8 → 1.3.0

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 (165) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/client/assets/{HomeView-CgEri1kD.js → HomeView-Bi2bkUKf.js} +1 -1
  3. package/dist/client/assets/{ResourceDetailView-DUJZbegl.css → ResourceDetailView-BkTSg91z.css} +1 -1
  4. package/dist/client/assets/ResourceDetailView-DyuSovH9.js +1 -0
  5. package/dist/client/assets/ResourcesView-CU0VbNy5.js +1 -0
  6. package/dist/client/assets/ResourcesView-zgV8Nq7w.css +1 -0
  7. package/dist/client/assets/{ServerDashboard-B3O-crvv.js → ServerDashboard-BGyyZAti.js} +1 -1
  8. package/dist/client/assets/{ServerDetail-CXg8rI3q.css → ServerDetail-CPNAFBPM.css} +1 -1
  9. package/dist/client/assets/ServerDetail-bcQ8BVXR.js +2 -0
  10. package/dist/client/assets/{ServerListView-SlZN8ppC.js → ServerListView-yQPVJFHG.js} +1 -1
  11. package/dist/client/assets/{ServerStatusTags.vue_vue_type_script_setup_true_lang-DmGg4uuV.js → ServerStatusTags.vue_vue_type_script_setup_true_lang-C8gQlxGE.js} +1 -1
  12. package/dist/client/assets/{SettingsView-D8fiOG0O.js → SettingsView-B1DxbFP3.js} +1 -1
  13. package/dist/client/assets/ToolCallDialog-BQ9UJZ_-.css +1 -0
  14. package/dist/client/assets/ToolCallDialog-DEapCO06.js +1 -0
  15. package/dist/client/assets/ToolsView-DA0u_bCw.js +1 -0
  16. package/dist/client/assets/ToolsView-cO61nMNr.css +1 -0
  17. package/dist/client/assets/{_baseClone-BYxCbA_9.js → _baseClone-B991Lvrt.js} +1 -1
  18. package/dist/client/assets/{el-form-item-ySymCPMr.js → el-form-item-DfWq_kSy.js} +1 -1
  19. package/dist/client/assets/{el-input-C85p6Nqj.js → el-input-5YzZrwir.js} +1 -1
  20. package/dist/client/assets/{el-loading-DIjKEx81.js → el-loading-DE3FcxNH.js} +1 -1
  21. package/dist/client/assets/{el-overlay-B_CxiSem.js → el-overlay-BTeTueuN.js} +1 -1
  22. package/dist/client/assets/{el-radio-group-BjkTCPRf.js → el-radio-group-Y1E2bxIW.js} +1 -1
  23. package/dist/client/assets/{el-skeleton-item-CupTKK6n.js → el-skeleton-item-DhgR50Jx.js} +1 -1
  24. package/dist/client/assets/{el-switch-BosIJ9jf.js → el-switch-fF--nMSD.js} +1 -1
  25. package/dist/client/assets/{el-tab-pane-BydxdJoc.js → el-tab-pane-rvS_KTwP.js} +1 -1
  26. package/dist/client/assets/{el-table-column-DV5TZOUW.js → el-table-column-B1O8mY47.js} +1 -1
  27. package/dist/client/assets/{index-xJkq2euk.css → index-Bzz3tYbS.css} +1 -1
  28. package/dist/client/assets/index-DkqV9kH4.js +2 -0
  29. package/dist/client/assets/{omit-DxDGRttI.js → omit-BIIebEYo.js} +1 -1
  30. package/dist/client/assets/{raf-Y9AoxecD.js → raf-Cj-gATZv.js} +1 -1
  31. package/dist/client/index.html +2 -2
  32. package/dist/server/shared/models/constants.d.ts +3 -0
  33. package/dist/server/shared/models/constants.d.ts.map +1 -0
  34. package/dist/server/shared/models/constants.js +2 -0
  35. package/dist/server/shared/models/index.d.ts +1 -0
  36. package/dist/server/shared/models/index.d.ts.map +1 -1
  37. package/dist/server/shared/models/index.js +1 -0
  38. package/dist/server/shared/types/index.d.ts +0 -1
  39. package/dist/server/shared/types/index.d.ts.map +1 -1
  40. package/dist/server/shared/types/index.js +0 -1
  41. package/dist/server/src/api/mcp/debug-response-wrapper.js +1 -1
  42. package/dist/server/src/api/mcp/gateway.d.ts.map +1 -1
  43. package/dist/server/src/api/mcp/gateway.js +23 -41
  44. package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
  45. package/dist/server/src/api/web/hub-tools.js +11 -0
  46. package/dist/server/src/api/web/mcp-status.js +2 -2
  47. package/dist/server/src/api/web/search.d.ts +2 -1
  48. package/dist/server/src/api/web/search.d.ts.map +1 -1
  49. package/dist/server/src/api/web/search.js +23 -10
  50. package/dist/server/src/api/web/servers.js +1 -1
  51. package/dist/server/src/api/ws/events.js +1 -1
  52. package/dist/server/src/api/ws/ws-handler.js +1 -1
  53. package/dist/server/src/app.js +1 -1
  54. package/dist/server/src/cli/commands/tool-use.d.ts +10 -3
  55. package/dist/server/src/cli/commands/tool-use.d.ts.map +1 -1
  56. package/dist/server/src/cli/commands/tool-use.js +69 -30
  57. package/dist/server/src/config/config-change-logger.js +1 -1
  58. package/dist/server/src/config/config-loader.js +1 -1
  59. package/dist/server/src/config/config-manager.js +1 -1
  60. package/dist/server/src/config/config-migrator.d.ts +4 -48
  61. package/dist/server/src/config/config-migrator.d.ts.map +1 -1
  62. package/dist/server/src/config/config-migrator.js +2 -103
  63. package/dist/server/src/config/config-saver.js +1 -1
  64. package/dist/server/src/config/server-config-manager.js +1 -1
  65. package/dist/server/src/models/event.model.d.ts +0 -98
  66. package/dist/server/src/models/event.model.d.ts.map +1 -1
  67. package/dist/server/src/models/event.model.js +0 -4
  68. package/dist/server/src/models/server.model.d.ts +0 -2
  69. package/dist/server/src/models/server.model.d.ts.map +1 -1
  70. package/dist/server/src/models/system-tools.constants.d.ts +8 -3
  71. package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
  72. package/dist/server/src/models/system-tools.constants.js +5 -2
  73. package/dist/server/src/pid/manager.js +1 -1
  74. package/dist/server/src/pid/types.d.ts +0 -5
  75. package/dist/server/src/pid/types.d.ts.map +1 -1
  76. package/dist/server/src/server/dev-server.js +2 -2
  77. package/dist/server/src/server/runner.js +2 -2
  78. package/dist/server/src/server/startup.js +2 -2
  79. package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
  80. package/dist/server/src/services/connection/connection-manager.js +16 -21
  81. package/dist/server/src/services/connection/tool-cache.d.ts.map +1 -1
  82. package/dist/server/src/services/connection/tool-cache.js +10 -8
  83. package/dist/server/src/services/event-bus.service.d.ts +3 -1
  84. package/dist/server/src/services/event-bus.service.d.ts.map +1 -1
  85. package/dist/server/src/services/event-bus.service.js +1 -0
  86. package/dist/server/src/services/gateway/gateway.service.d.ts +1 -0
  87. package/dist/server/src/services/gateway/gateway.service.d.ts.map +1 -1
  88. package/dist/server/src/services/gateway/gateway.service.js +29 -7
  89. package/dist/server/src/services/gateway/request-handlers/call-tool-handler.d.ts +1 -2
  90. package/dist/server/src/services/gateway/request-handlers/call-tool-handler.d.ts.map +1 -1
  91. package/dist/server/src/services/gateway/request-handlers/call-tool-handler.js +24 -13
  92. package/dist/server/src/services/gateway/request-handlers/resources-handler.d.ts.map +1 -1
  93. package/dist/server/src/services/gateway/request-handlers/resources-handler.js +7 -3
  94. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
  95. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +28 -1
  96. package/dist/server/src/services/gateway/tool-list-generator.d.ts +14 -19
  97. package/dist/server/src/services/gateway/tool-list-generator.d.ts.map +1 -1
  98. package/dist/server/src/services/gateway/tool-list-generator.js +221 -80
  99. package/dist/server/src/services/hub-manager.service.d.ts.map +1 -1
  100. package/dist/server/src/services/hub-manager.service.js +21 -4
  101. package/dist/server/src/services/hub-tools/instance-selector.js +1 -1
  102. package/dist/server/src/services/hub-tools/resource-generator.d.ts +0 -21
  103. package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
  104. package/dist/server/src/services/hub-tools/resource-generator.js +15 -15
  105. package/dist/server/src/services/hub-tools/server-selector.js +1 -1
  106. package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts +1 -0
  107. package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
  108. package/dist/server/src/services/hub-tools/system-tool-definitions.js +25 -1
  109. package/dist/server/src/services/hub-tools.service.d.ts +21 -4
  110. package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
  111. package/dist/server/src/services/hub-tools.service.js +74 -13
  112. package/dist/server/src/services/log-storage.service.js +1 -1
  113. package/dist/server/src/services/system-tool-handler.d.ts.map +1 -1
  114. package/dist/server/src/services/system-tool-handler.js +10 -2
  115. package/dist/server/src/utils/error-handler.js +1 -1
  116. package/dist/server/src/utils/index.d.ts +1 -1
  117. package/dist/server/src/utils/index.d.ts.map +1 -1
  118. package/dist/server/src/utils/index.js +1 -1
  119. package/dist/server/src/utils/instance-id.d.ts +0 -8
  120. package/dist/server/src/utils/instance-id.d.ts.map +1 -1
  121. package/dist/server/src/utils/instance-id.js +1 -1
  122. package/dist/server/src/utils/json-utils.js +4 -4
  123. package/dist/server/src/utils/log-rotator.d.ts +0 -15
  124. package/dist/server/src/utils/log-rotator.d.ts.map +1 -1
  125. package/dist/server/src/utils/log-rotator.js +0 -18
  126. package/dist/server/src/utils/logger/index.d.ts +0 -22
  127. package/dist/server/src/utils/logger/index.d.ts.map +1 -1
  128. package/dist/server/src/utils/logger/index.js +0 -29
  129. package/dist/server/src/utils/port-checker.js +1 -1
  130. package/dist/server/src/utils/sort-utils.d.ts +0 -16
  131. package/dist/server/src/utils/sort-utils.d.ts.map +1 -1
  132. package/dist/server/src/utils/sort-utils.js +0 -42
  133. package/dist/server/src/utils/transports/stdio-transport.js +1 -1
  134. package/dist/server/src/utils/transports/streamable-http-transport.js +1 -1
  135. package/dist/server/src/utils/transports/transport-factory.d.ts.map +1 -1
  136. package/dist/server/src/utils/transports/transport-factory.js +26 -3
  137. package/dist/server/tests/contract/mcp-protocol/initialize.test.js +1 -1
  138. package/dist/server/tests/contract/mcp-protocol/tools-call.test.js +1 -1
  139. package/dist/server/tests/contract/mcp-protocol/tools-list.test.js +1 -1
  140. package/dist/server/tests/integration/gateway/fault-tolerance.test.js +1 -1
  141. package/dist/server/tests/integration/gateway/mcp-connection.test.js +1 -1
  142. package/dist/server/tests/types/logger-test-helpers.d.ts +1 -1
  143. package/dist/server/tests/types/logger-test-helpers.d.ts.map +1 -1
  144. package/dist/server/tests/unit/api/search.test.d.ts +2 -0
  145. package/dist/server/tests/unit/api/search.test.d.ts.map +1 -0
  146. package/dist/server/tests/unit/api/search.test.js +61 -0
  147. package/dist/server/tests/unit/config/config-migrator.test.js +45 -105
  148. package/dist/server/tests/unit/config/config-saver.test.js +1 -1
  149. package/dist/server/tests/unit/server/runner.test.js +5 -6
  150. package/dist/server/tests/unit/services/gateway-logging.test.js +1 -1
  151. package/dist/server/tests/unit/services/hub-manager-service.test.js +4 -5
  152. package/dist/server/tests/unit/services/hub-tools.service.test.js +80 -3
  153. package/dist/server/tests/unit/utils/log-rotator.test.js +1 -15
  154. package/dist/server/tests/unit/utils/logger.test.js +1 -23
  155. package/dist/server/tests/unit/utils/sort-utils.test.js +1 -92
  156. package/package.json +1 -3
  157. package/dist/client/assets/ResourceDetailView-B8Qo1_jK.js +0 -1
  158. package/dist/client/assets/ResourcesView-B12FzUdo.js +0 -1
  159. package/dist/client/assets/ResourcesView-Cc8RHtia.css +0 -1
  160. package/dist/client/assets/ServerDetail-Bz5_9yOY.js +0 -2
  161. package/dist/client/assets/ToolCallDialog-BhdPX-Kf.css +0 -1
  162. package/dist/client/assets/ToolCallDialog-DYEdhnCk.js +0 -1
  163. package/dist/client/assets/ToolsView-BreAl-yn.js +0 -1
  164. package/dist/client/assets/ToolsView-BxgXvPC3.css +0 -1
  165. package/dist/client/assets/index-kC4mf0Vo.js +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../../../../tests/unit/api/search.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { filterByAggregatedTools } from '../../../src/api/web/search.js';
3
+ function mockConfig(aggregatedTools = []) {
4
+ return {
5
+ template: {
6
+ type: 'stdio',
7
+ command: 'test',
8
+ args: [],
9
+ env: {},
10
+ headers: {},
11
+ aggregatedTools,
12
+ timeout: 30000
13
+ },
14
+ instances: [],
15
+ tagDefinitions: []
16
+ };
17
+ }
18
+ function mockTool(name, serverName = 'test-server') {
19
+ return { name, description: `Tool ${name}`, serverName };
20
+ }
21
+ describe('filterByAggregatedTools', () => {
22
+ it('should skip tools when aggregatedTools is empty', () => {
23
+ const tools = [mockTool('tool1'), mockTool('tool2')];
24
+ const getConfig = () => mockConfig([]);
25
+ const result = filterByAggregatedTools(tools, getConfig);
26
+ expect(result).toHaveLength(0);
27
+ });
28
+ it('should only include tools in aggregatedTools list', () => {
29
+ const tools = [mockTool('tool1'), mockTool('tool2'), mockTool('tool3')];
30
+ const getConfig = () => mockConfig(['tool1', 'tool3']);
31
+ const result = filterByAggregatedTools(tools, getConfig);
32
+ expect(result).toHaveLength(2);
33
+ expect(result.map((t) => t.name)).toEqual(['tool1', 'tool3']);
34
+ });
35
+ it('should skip tools when server config not found', () => {
36
+ const tools = [mockTool('tool1')];
37
+ const getConfig = () => undefined;
38
+ const result = filterByAggregatedTools(tools, getConfig);
39
+ expect(result).toHaveLength(0);
40
+ });
41
+ it('should filter tools from multiple servers independently', () => {
42
+ const tools = [
43
+ mockTool('a', 'server-a'),
44
+ mockTool('b', 'server-a'),
45
+ mockTool('x', 'server-b'),
46
+ mockTool('y', 'server-b')
47
+ ];
48
+ const configs = {
49
+ 'server-a': mockConfig(['a']),
50
+ 'server-b': mockConfig([])
51
+ };
52
+ const getConfig = (name) => configs[name];
53
+ const result = filterByAggregatedTools(tools, getConfig);
54
+ expect(result).toHaveLength(1);
55
+ expect(result[0].name).toBe('a');
56
+ });
57
+ it('should return empty array when no tools provided', () => {
58
+ const result = filterByAggregatedTools([], () => mockConfig(['tool1']));
59
+ expect(result).toHaveLength(0);
60
+ });
61
+ });
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
- import { dryRunMigration, checkMigrationStatus, migrateConfig, rollbackMigration, resolveInstanceConfig, getEnabledInstances } from '../../../src/config/config-migrator.js';
5
+ import { migrateConfig, resolveInstanceConfig } from '../../../src/config/config-migrator.js';
6
6
  import { SystemConfigSchema, isLegacyV1Config } from '../../../src/config/config.schema.js';
7
7
  describe('Config Migrator', () => {
8
8
  let tempDir;
@@ -85,59 +85,15 @@ describe('Config Migrator', () => {
85
85
  const config = createLegacyV1Config();
86
86
  fs.writeFileSync(testConfigPath, JSON.stringify(config, null, 2));
87
87
  };
88
- describe('checkMigrationStatus', () => {
89
- it('should return "not found" for non-existent file', () => {
90
- const result = checkMigrationStatus(testConfigPath);
91
- expect(result.exists).toBe(false);
92
- expect(result.version).toBe('unknown');
93
- expect(result.canMigrate).toBe(false);
94
- });
95
- it('should detect v1.0 configuration', () => {
96
- writeLegacyV1Config();
97
- const result = checkMigrationStatus(testConfigPath);
98
- expect(result.exists).toBe(true);
99
- expect(result.version).toBe('v1');
100
- expect(result.canMigrate).toBe(true);
101
- });
102
- it('should detect v1.1 configuration', () => {
103
- const v1_1Config = {
104
- version: '1.1.0',
105
- system: {
106
- host: 'localhost',
107
- port: 7788,
108
- language: 'zh',
109
- theme: 'system',
110
- logging: {
111
- level: 'info',
112
- rotationAge: '7d',
113
- jsonPretty: true,
114
- mcpCommDebug: false,
115
- apiDebug: false,
116
- gatewayDebug: false
117
- }
118
- },
119
- security: {
120
- allowedNetworks: ['127.0.0.1'],
121
- maxConcurrentConnections: 50,
122
- connectionTimeout: 30000,
123
- idleConnectionTimeout: 300000,
124
- maxConnections: 50
125
- },
126
- servers: {},
127
- tagDefinitions: []
128
- };
129
- fs.writeFileSync(testConfigPath, JSON.stringify(v1_1Config, null, 2));
130
- const result = checkMigrationStatus(testConfigPath);
131
- expect(result.exists).toBe(true);
132
- expect(result.version).toBe('v1.1');
133
- expect(result.canMigrate).toBe(false);
134
- });
135
- });
136
- describe('dryRunMigration', () => {
137
- it('should perform migration without modifying files', () => {
88
+ describe('migrateConfig (actual migration)', () => {
89
+ it('should perform dry run without modifying files', () => {
138
90
  writeLegacyV1Config();
139
91
  const originalContent = fs.readFileSync(testConfigPath, 'utf8');
140
- const result = dryRunMigration(testConfigPath);
92
+ const result = migrateConfig(testConfigPath, {
93
+ dryRun: true,
94
+ createBackup: false,
95
+ validateAfterMigration: true
96
+ });
141
97
  expect(result.success).toBe(true);
142
98
  expect(result.migratedConfig).toBeDefined();
143
99
  expect(result.backupPath).toBeUndefined();
@@ -145,9 +101,13 @@ describe('Config Migrator', () => {
145
101
  const currentContent = fs.readFileSync(testConfigPath, 'utf8');
146
102
  expect(currentContent).toBe(originalContent);
147
103
  });
148
- it('should convert servers to servers correctly', () => {
104
+ it('should convert servers correctly (dry run)', () => {
149
105
  writeLegacyV1Config();
150
- const result = dryRunMigration(testConfigPath);
106
+ const result = migrateConfig(testConfigPath, {
107
+ dryRun: true,
108
+ createBackup: false,
109
+ validateAfterMigration: true
110
+ });
151
111
  expect(result.success).toBe(true);
152
112
  expect(result.migratedConfig).toBeDefined();
153
113
  const migrated = result.migratedConfig;
@@ -156,7 +116,6 @@ describe('Config Migrator', () => {
156
116
  expect(Object.keys(migrated.servers)).toHaveLength(2);
157
117
  expect(migrated.servers['test-server-1']).toBeDefined();
158
118
  expect(migrated.servers['test-server-2']).toBeDefined();
159
- // Check server 1
160
119
  const server1 = migrated.servers['test-server-1'];
161
120
  expect(server1.template.command).toBe('npx test-server-1');
162
121
  expect(server1.template.args).toEqual(['--verbose']);
@@ -167,17 +126,19 @@ describe('Config Migrator', () => {
167
126
  expect(server1.instances[0].id).toMatch(/test-server-1-[0-9a-f]{8}/);
168
127
  expect(server1.instances[0].enabled).toBe(true);
169
128
  });
170
- it('should preserve system and security config', () => {
129
+ it('should preserve system and security config (dry run)', () => {
171
130
  writeLegacyV1Config();
172
131
  const v1Config = createLegacyV1Config();
173
- const result = dryRunMigration(testConfigPath);
132
+ const result = migrateConfig(testConfigPath, {
133
+ dryRun: true,
134
+ createBackup: false,
135
+ validateAfterMigration: true
136
+ });
174
137
  expect(result.success).toBe(true);
175
138
  const migrated = result.migratedConfig;
176
139
  expect(migrated.system).toEqual(v1Config.system);
177
140
  expect(migrated.security).toEqual(v1Config.security);
178
141
  });
179
- });
180
- describe('migrateConfig (actual migration)', () => {
181
142
  it('should perform actual migration and validate the result', () => {
182
143
  writeLegacyV1Config();
183
144
  const originalContent = fs.readFileSync(testConfigPath, 'utf8');
@@ -208,40 +169,15 @@ describe('Config Migrator', () => {
208
169
  expect(validation.success).toBe(true);
209
170
  });
210
171
  });
211
- describe('rollbackMigration', () => {
212
- it('should rollback to v1.0 using backup', () => {
213
- writeLegacyV1Config();
214
- const originalContent = fs.readFileSync(testConfigPath, 'utf8');
215
- // Perform migration first
216
- const migrationResult = migrateConfig(testConfigPath, {
217
- createBackup: true,
218
- validateAfterMigration: true
219
- });
220
- expect(migrationResult.success).toBe(true);
221
- expect(migrationResult.backupPath).toBeDefined();
222
- // Verify file is v1.1
223
- const migratedContent = fs.readFileSync(testConfigPath, 'utf8');
224
- expect(migratedContent).not.toBe(originalContent);
225
- // Rollback
226
- const rollbackResult = rollbackMigration(testConfigPath, migrationResult.backupPath);
227
- expect(rollbackResult.success).toBe(true);
228
- // Verify file is back to original
229
- const rolledBackContent = fs.readFileSync(testConfigPath, 'utf8');
230
- expect(rolledBackContent).toBe(originalContent);
231
- });
232
- it('should fail rollback for non-existent backup', () => {
233
- writeLegacyV1Config();
234
- const nonExistentBackup = path.join(tempDir, 'non-existent-backup.json');
235
- const result = rollbackMigration(testConfigPath, nonExistentBackup);
236
- expect(result.success).toBe(false);
237
- expect(result.error).toContain('not found');
238
- });
239
- });
240
172
  describe('v1.1 Configuration Helpers', () => {
241
173
  describe('resolveInstanceConfig', () => {
242
174
  it('should resolve instance configuration by merging template and instance', () => {
243
175
  writeLegacyV1Config();
244
- const migrationResult = dryRunMigration(testConfigPath);
176
+ const migrationResult = migrateConfig(testConfigPath, {
177
+ dryRun: true,
178
+ createBackup: false,
179
+ validateAfterMigration: true
180
+ });
245
181
  expect(migrationResult.success).toBe(true);
246
182
  const serverConfig = migrationResult.migratedConfig.servers['test-server-1'];
247
183
  const resolved = resolveInstanceConfig(serverConfig);
@@ -255,7 +191,11 @@ describe('Config Migrator', () => {
255
191
  });
256
192
  it('should resolve specific instance by ID', () => {
257
193
  writeLegacyV1Config();
258
- const migrationResult = dryRunMigration(testConfigPath);
194
+ const migrationResult = migrateConfig(testConfigPath, {
195
+ dryRun: true,
196
+ createBackup: false,
197
+ validateAfterMigration: true
198
+ });
259
199
  expect(migrationResult.success).toBe(true);
260
200
  const serverConfig = migrationResult.migratedConfig.servers['test-server-1'];
261
201
  const instanceId = serverConfig.instances[0].id;
@@ -265,25 +205,17 @@ describe('Config Migrator', () => {
265
205
  });
266
206
  it('should return null for non-existent instance ID', () => {
267
207
  writeLegacyV1Config();
268
- const migrationResult = dryRunMigration(testConfigPath);
208
+ const migrationResult = migrateConfig(testConfigPath, {
209
+ dryRun: true,
210
+ createBackup: false,
211
+ validateAfterMigration: true
212
+ });
269
213
  expect(migrationResult.success).toBe(true);
270
214
  const serverConfig = migrationResult.migratedConfig.servers['test-server-1'];
271
215
  const resolved = resolveInstanceConfig(serverConfig, 'non-existent-id');
272
216
  expect(resolved).toBeNull();
273
217
  });
274
218
  });
275
- describe('getEnabledInstances', () => {
276
- it('should return only enabled instances', () => {
277
- writeLegacyV1Config();
278
- const migrationResult = dryRunMigration(testConfigPath);
279
- expect(migrationResult.success).toBe(true);
280
- const serverConfig = migrationResult.migratedConfig.servers['test-server-1'];
281
- const enabledInstances = getEnabledInstances(serverConfig);
282
- expect(enabledInstances).toHaveLength(1);
283
- expect(enabledInstances[0].instance.enabled).toBe(true);
284
- expect(enabledInstances[0].resolved).not.toBeNull();
285
- });
286
- });
287
219
  });
288
220
  describe('Migration Edge Cases', () => {
289
221
  it('should handle empty servers list', () => {
@@ -292,7 +224,11 @@ describe('Config Migrator', () => {
292
224
  servers: {}
293
225
  };
294
226
  fs.writeFileSync(testConfigPath, JSON.stringify(emptyV1Config, null, 2));
295
- const result = dryRunMigration(testConfigPath);
227
+ const result = migrateConfig(testConfigPath, {
228
+ dryRun: true,
229
+ createBackup: false,
230
+ validateAfterMigration: true
231
+ });
296
232
  expect(result.success).toBe(true);
297
233
  expect(result.migratedConfig.servers).toEqual({});
298
234
  });
@@ -310,7 +246,11 @@ describe('Config Migrator', () => {
310
246
  }
311
247
  };
312
248
  fs.writeFileSync(testConfigPath, JSON.stringify(minimalV1Config, null, 2));
313
- const result = dryRunMigration(testConfigPath);
249
+ const result = migrateConfig(testConfigPath, {
250
+ dryRun: true,
251
+ createBackup: false,
252
+ validateAfterMigration: true
253
+ });
314
254
  expect(result.success).toBe(true);
315
255
  expect(result.migratedConfig.servers['minimal-server']).toBeDefined();
316
256
  });
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  // Mock fs module
6
6
  vi.mock('fs');
7
7
  vi.mock('path');
8
- vi.mock('@utils/logger.js', () => ({
8
+ vi.mock('@utils/logger/index.js', () => ({
9
9
  logger: {
10
10
  debug: vi.fn(),
11
11
  error: vi.fn()
@@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { runServer } from '../../../src/server/runner.js';
3
3
  import { buildApp } from '../../../src/app.js';
4
4
  import { configManager } from '../../../src/config/config-manager.js';
5
- import { logger } from '../../../src/utils/logger.js';
6
- import { mcpConnectionManager } from '../../../src/services/mcp-connection-manager.js';
5
+ import { logger } from '../../../src/utils/logger/index.js';
6
+ import { mcpConnectionManager } from '../../../src/services/connection/index.js';
7
7
  import { PidManager } from '../../../src/pid/manager.js';
8
8
  import { checkPort, checkPortWithExit } from '../../../src/utils/port-checker.js';
9
9
  // Mock resolveInstanceConfig to return a valid resolved config
@@ -16,8 +16,7 @@ vi.mock('@config/config-migrator.js', () => ({
16
16
  aggregatedTools: [],
17
17
  tags: {},
18
18
  enabled: true
19
- })),
20
- getEnabledInstances: vi.fn()
19
+ }))
21
20
  }));
22
21
  // Mock all dependencies
23
22
  vi.mock('@src/app.js', () => ({
@@ -31,7 +30,7 @@ vi.mock('@config/config-manager.js', () => ({
31
30
  addServerInstance: vi.fn()
32
31
  }
33
32
  }));
34
- vi.mock('@utils/logger.js', () => ({
33
+ vi.mock('@utils/logger/index.js', () => ({
35
34
  logger: {
36
35
  info: vi.fn(),
37
36
  error: vi.fn(),
@@ -43,7 +42,7 @@ vi.mock('@utils/logger.js', () => ({
43
42
  SERVER: { module: 'Server' }
44
43
  }
45
44
  }));
46
- vi.mock('@services/mcp-connection-manager.js', () => ({
45
+ vi.mock('@services/connection/index.js', () => ({
47
46
  mcpConnectionManager: {
48
47
  connect: vi.fn(() => Promise.resolve(true)),
49
48
  disconnectAll: vi.fn()
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from 'vitest';
2
- import { GatewayService } from '../../../src/services/gateway.service.js';
2
+ import { GatewayService } from '../../../src/services/gateway/index.js';
3
3
  describe('GatewayService Logging Helpers', () => {
4
4
  // Access private methods using type assertion for testing
5
5
  const gateway = new GatewayService();
@@ -1,11 +1,10 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { HubManagerService } from '../../../src/services/hub-manager.service.js';
3
- import { mcpConnectionManager } from '../../../src/services/mcp-connection-manager.js';
3
+ import { mcpConnectionManager } from '../../../src/services/connection/index.js';
4
4
  import { configManager } from '../../../src/config/config-manager.js';
5
5
  // Mock resolveInstanceConfig to return a valid resolved config
6
6
  vi.mock('@config/config-migrator.js', () => ({
7
- resolveInstanceConfig: vi.fn(),
8
- getEnabledInstances: vi.fn()
7
+ resolveInstanceConfig: vi.fn()
9
8
  }));
10
9
  // Mock dependencies
11
10
  vi.mock('@config/config-manager.js', () => ({
@@ -23,13 +22,13 @@ vi.mock('@config/config-manager.js', () => ({
23
22
  removeServerInstance: vi.fn()
24
23
  }
25
24
  }));
26
- vi.mock('@services/mcp-connection-manager.js', () => ({
25
+ vi.mock('@services/connection/index.js', () => ({
27
26
  mcpConnectionManager: {
28
27
  connect: vi.fn(),
29
28
  disconnect: vi.fn(() => Promise.resolve())
30
29
  }
31
30
  }));
32
- vi.mock('@utils/logger.js', () => ({
31
+ vi.mock('@utils/logger/index.js', () => ({
33
32
  logger: {
34
33
  info: vi.fn(),
35
34
  error: vi.fn(),
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { HubToolsService } from '../../../src/services/hub-tools.service.js';
3
3
  import { hubManager } from '../../../src/services/hub-manager.service.js';
4
- import { mcpConnectionManager } from '../../../src/services/mcp-connection-manager.js';
4
+ import { mcpConnectionManager } from '../../../src/services/connection/index.js';
5
5
  // Mock dependencies
6
6
  vi.mock('@services/hub-manager.service.js');
7
- vi.mock('@services/mcp-connection-manager.js');
7
+ vi.mock('@services/connection/index.js');
8
8
  describe('HubToolsService', () => {
9
9
  let hubToolsService;
10
10
  beforeEach(() => {
@@ -320,6 +320,82 @@ describe('HubToolsService', () => {
320
320
  });
321
321
  expect(servers).not.toHaveProperty('Disconnected Server');
322
322
  });
323
+ it('should include tag-match-unique servers with multiple connected instances', async () => {
324
+ // Arrange: 4-instance server with tag-match-unique strategy, all connected
325
+ const mockServers = [
326
+ {
327
+ name: 'multi-instance-server',
328
+ config: {
329
+ template: {
330
+ type: 'stdio',
331
+ command: 'test-command',
332
+ args: [],
333
+ env: {},
334
+ headers: {},
335
+ aggregatedTools: [],
336
+ timeout: 30000,
337
+ description: 'Multi-instance test server',
338
+ tags: {}
339
+ },
340
+ instances: [
341
+ {
342
+ id: 'inst-0',
343
+ index: 0,
344
+ enabled: false,
345
+ args: [],
346
+ env: {},
347
+ headers: {},
348
+ tags: { Env: 'dev' }
349
+ },
350
+ {
351
+ id: 'inst-1',
352
+ index: 1,
353
+ enabled: false,
354
+ args: [],
355
+ env: {},
356
+ headers: {},
357
+ tags: { Env: 'test' }
358
+ },
359
+ {
360
+ id: 'inst-2',
361
+ index: 2,
362
+ enabled: false,
363
+ args: [],
364
+ env: {},
365
+ headers: {},
366
+ tags: { Env: 'prod' }
367
+ },
368
+ {
369
+ id: 'inst-3',
370
+ index: 3,
371
+ enabled: false,
372
+ args: [],
373
+ env: {},
374
+ headers: {},
375
+ tags: { Env: 'staging' }
376
+ }
377
+ ],
378
+ tagDefinitions: [],
379
+ instanceSelectionStrategy: 'tag-match-unique'
380
+ }
381
+ }
382
+ ];
383
+ vi.mocked(hubManager.getAllServers).mockReturnValue(mockServers);
384
+ vi.mocked(hubManager.getServerInstancesByName).mockReturnValue(mockServers[0].config.instances);
385
+ vi.mocked(hubManager.getServerByName).mockReturnValue(mockServers[0].config);
386
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0, 1, 2, 3]);
387
+ vi.mocked(mcpConnectionManager.getStatus).mockReturnValue({
388
+ connected: true,
389
+ lastCheck: Date.now(),
390
+ toolsCount: 1,
391
+ resourcesCount: 0
392
+ });
393
+ // Act
394
+ const servers = await hubToolsService.listServers();
395
+ // Assert: tag-match-unique server with multiple connected instances must be included
396
+ expect(servers).toHaveProperty('multi-instance-server');
397
+ expect(servers['multi-instance-server']).toBe('Multi-instance test server');
398
+ });
323
399
  });
324
400
  describe('listToolsInServer', () => {
325
401
  it('should return tool summaries from a specific server (without inputSchema)', async () => {
@@ -615,7 +691,8 @@ describe('HubToolsService', () => {
615
691
  expect(systemToolNames).toContain('call_tool');
616
692
  expect(systemToolNames).toContain('update_server_description');
617
693
  expect(systemToolNames).toContain('list_tags');
618
- expect(systemToolNames).toHaveLength(6);
694
+ expect(systemToolNames).toContain('search_tools');
695
+ expect(systemToolNames).toHaveLength(7);
619
696
  // Assert server tools - should have only name and description
620
697
  expect(allTools['Server 1'].tools).toEqual(expectedToolSummariesServer1);
621
698
  expect(allTools['Server 2'].tools).toEqual(expectedToolSummariesServer2);
@@ -79,21 +79,7 @@ describe('LogRotator', () => {
79
79
  const latest = logRotator.getLatestLogFilePath();
80
80
  expect(latest).toBeNull();
81
81
  });
82
- it('should maintain backward compatibility with getCurrentLogFilePath', () => {
83
- logRotator = new LogRotator(tempLogDir, 'mcp-hub', undefined, () => originalConfig);
84
- // When no files exist, getCurrentLogFilePath will create a new path (but not the file)
85
- const path1 = logRotator.getCurrentLogFilePath();
86
- expect(path.basename(path1)).toMatch(/^mcp-hub\.\d{8}_\d{9}\.log$/);
87
- // Actually create the file on disk
88
- fs.writeFileSync(path1, 'test content 1');
89
- // Create an older file
90
- const oldFile = path.join(tempLogDir, 'mcp-hub.20260301_100000000.log');
91
- fs.writeFileSync(oldFile, 'test content 2');
92
- // When files exist, getCurrentLogFilePath should return the latest one
93
- const path2 = logRotator.getCurrentLogFilePath();
94
- expect(path2).toBe(path1); // Should return the newer one we just created
95
- });
96
- it('should get current log file path with custom base name', () => {
82
+ it('should create new log file path with custom base name', () => {
97
83
  logRotator = new LogRotator(tempLogDir, 'custom-log', undefined, () => originalConfig);
98
84
  const currentPath = logRotator.createNewLogFilePath();
99
85
  const basename = path.basename(currentPath);
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
- import { Logger, logWithColor } from '../../../src/utils/logger.js';
5
+ import { Logger } from '../../../src/utils/logger/index.js';
6
6
  import { setJsonPrettyConfigGetter, setDevModeEnabled } from '../../../src/utils/json-utils.js';
7
7
  import { createColoredLogMessage, createLogMessage, getCallerInfo, formatCallerInfo } from '../../../src/utils/logger/log-formatter.js';
8
8
  describe('Logger', () => {
@@ -363,28 +363,6 @@ describe('Logger', () => {
363
363
  expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('file line 3'));
364
364
  vi.restoreAllMocks();
365
365
  });
366
- it('should handle logWithColor function with traceId and spanId', () => {
367
- // Reset logger instance to avoid interference between tests
368
- const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => { });
369
- logWithColor('colored message', 'plain message', {
370
- pid: 123,
371
- serverName: 'test-server',
372
- traceId: '1234567890abcdef1234567890abcdef',
373
- spanId: 'abcdef1234567890'
374
- });
375
- expect(consoleInfoSpy).toHaveBeenCalledWith(expect.stringContaining('[TID:1234567890abcdef1234567890abcdef]'));
376
- expect(consoleInfoSpy).toHaveBeenCalledWith(expect.stringContaining('[SID:abcdef1234567890]'));
377
- // Restore console methods
378
- consoleInfoSpy.mockRestore();
379
- });
380
- it('should handle logWithColor function without context', () => {
381
- // Reset logger instance to avoid interference between tests
382
- const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => { });
383
- logWithColor('colored message', 'plain message');
384
- expect(consoleInfoSpy).toHaveBeenCalled();
385
- // Restore console methods
386
- consoleInfoSpy.mockRestore();
387
- });
388
366
  describe('dev log rotation', () => {
389
367
  it('should create devLogRotator when enableDevLog is called', () => {
390
368
  // Mock fs modules to avoid file system operations
@@ -1,50 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { sortObjectKeys, sortObjectKeysCaseInsensitive, sortObjectKeysDeep, sortServerConfigEnvHeaders } from '../../../src/utils/sort-utils.js';
2
+ import { sortObjectKeysCaseInsensitive, sortServerConfigEnvHeaders } from '../../../src/utils/sort-utils.js';
3
3
  describe('sort-utils', () => {
4
- describe('sortObjectKeys', () => {
5
- it('should sort object keys alphabetically using localeCompare', () => {
6
- const obj = {
7
- zebra: 'value3',
8
- apple: 'value1',
9
- banana: 'value2'
10
- };
11
- const result = sortObjectKeys(obj);
12
- expect(Object.keys(result)).toEqual(['apple', 'banana', 'zebra']);
13
- expect(result).toEqual({
14
- apple: 'value1',
15
- banana: 'value2',
16
- zebra: 'value3'
17
- });
18
- });
19
- it('should return the same object if input is null or undefined', () => {
20
- expect(sortObjectKeys(null)).toBe(null);
21
- expect(sortObjectKeys(undefined)).toBe(undefined);
22
- });
23
- it('should return the same object if input is an array', () => {
24
- const arr = [3, 1, 2];
25
- expect(sortObjectKeys(arr)).toBe(arr);
26
- });
27
- it('should return the same object if input is not an object', () => {
28
- expect(sortObjectKeys('string')).toBe('string');
29
- expect(sortObjectKeys(42)).toBe(42);
30
- expect(sortObjectKeys(true)).toBe(true);
31
- });
32
- it('should preserve the original object (return a new object)', () => {
33
- const obj = {
34
- zebra: 'value3',
35
- apple: 'value1'
36
- };
37
- const result = sortObjectKeys(obj);
38
- expect(result).not.toBe(obj);
39
- expect(obj).toEqual({
40
- zebra: 'value3',
41
- apple: 'value1'
42
- });
43
- });
44
- it('should handle empty object', () => {
45
- expect(sortObjectKeys({})).toEqual({});
46
- });
47
- });
48
4
  describe('sortObjectKeysCaseInsensitive', () => {
49
5
  it('should sort object keys alphabetically, case-insensitive', () => {
50
6
  const obj = {
@@ -77,53 +33,6 @@ describe('sort-utils', () => {
77
33
  expect(sortObjectKeysCaseInsensitive(undefined)).toBe(undefined);
78
34
  });
79
35
  });
80
- describe('sortObjectKeysDeep', () => {
81
- it('should recursively sort all object keys in a nested structure', () => {
82
- const obj = {
83
- zebra: {
84
- delta: 'value4',
85
- alpha: 'value1'
86
- },
87
- apple: {
88
- charlie: 'value3',
89
- bravo: 'value2'
90
- }
91
- };
92
- const result = sortObjectKeysDeep(obj);
93
- expect(Object.keys(result)).toEqual(['apple', 'zebra']);
94
- expect(Object.keys(result.apple)).toEqual(['bravo', 'charlie']);
95
- expect(Object.keys(result.zebra)).toEqual(['alpha', 'delta']);
96
- });
97
- it('should handle arrays and recursively sort objects within arrays', () => {
98
- const obj = {
99
- items: [
100
- { zebra: 'value3', apple: 'value1' },
101
- { delta: 'value4', alpha: 'value1' }
102
- ]
103
- };
104
- const result = sortObjectKeysDeep(obj);
105
- expect(Array.isArray(result.items)).toBe(true);
106
- expect(Object.keys(result.items[0])).toEqual(['apple', 'zebra']);
107
- expect(Object.keys(result.items[1])).toEqual(['alpha', 'delta']);
108
- });
109
- it('should handle mixed types in nested structure', () => {
110
- const obj = {
111
- number: 42,
112
- string: 'hello',
113
- boolean: true,
114
- array: [1, 2, 3],
115
- object: {
116
- nested: {
117
- z: 'last',
118
- a: 'first'
119
- }
120
- }
121
- };
122
- const result = sortObjectKeysDeep(obj);
123
- expect(Object.keys(result)).toEqual(['array', 'boolean', 'number', 'object', 'string']);
124
- expect(Object.keys(result.object.nested)).toEqual(['a', 'z']);
125
- });
126
- });
127
36
  describe('sortServerConfigEnvHeaders', () => {
128
37
  it('should sort env and headers objects in a server configuration', () => {
129
38
  const config = {