@loop_ouroboros/mcp-hub-lite 1.3.1 → 1.3.3

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 (34) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +8 -3
  3. package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
  4. package/dist/server/src/api/web/hub-tools.js +2 -2
  5. package/dist/server/src/api/web/search.d.ts +1 -1
  6. package/dist/server/src/api/web/search.d.ts.map +1 -1
  7. package/dist/server/src/api/web/search.js +18 -16
  8. package/dist/server/src/models/system-tools.constants.d.ts +1 -0
  9. package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
  10. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
  11. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +3 -2
  12. package/dist/server/src/services/hub-tools/index.d.ts +1 -0
  13. package/dist/server/src/services/hub-tools/index.d.ts.map +1 -1
  14. package/dist/server/src/services/hub-tools/index.js +1 -0
  15. package/dist/server/src/services/hub-tools/resource-generator.d.ts +1 -1
  16. package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
  17. package/dist/server/src/services/hub-tools/resource-generator.js +11 -29
  18. package/dist/server/src/services/hub-tools/server-metadata-cache.d.ts +19 -0
  19. package/dist/server/src/services/hub-tools/server-metadata-cache.d.ts.map +1 -0
  20. package/dist/server/src/services/hub-tools/server-metadata-cache.js +117 -0
  21. package/dist/server/src/services/hub-tools/server-selector.d.ts +1 -1
  22. package/dist/server/src/services/hub-tools/server-selector.d.ts.map +1 -1
  23. package/dist/server/src/services/hub-tools/server-selector.js +3 -8
  24. package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
  25. package/dist/server/src/services/hub-tools/system-tool-definitions.js +8 -1
  26. package/dist/server/src/services/hub-tools.service.d.ts +1 -1
  27. package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
  28. package/dist/server/src/services/hub-tools.service.js +62 -88
  29. package/dist/server/src/services/system-tool-handler.js +1 -1
  30. package/dist/server/src/utils/search-matcher.d.ts +6 -0
  31. package/dist/server/src/utils/search-matcher.d.ts.map +1 -0
  32. package/dist/server/src/utils/search-matcher.js +24 -0
  33. package/dist/server/tests/unit/services/hub-tools.service.test.js +358 -8
  34. package/package.json +1 -1
@@ -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
@@ -789,15 +1084,17 @@ describe('HubToolsService', () => {
789
1084
  });
790
1085
  it('should throw error for non-existent server', async () => {
791
1086
  // Arrange
1087
+ vi.mocked(hubManager.getServerByName).mockReturnValue(undefined);
792
1088
  vi.mocked(hubManager.getServerInstancesByName).mockReturnValue([]);
793
1089
  // Act & Assert
794
- await expect(hubToolsService.readResource('hub://servers/NonExistent')).rejects.toThrow('Server not found or not connected');
1090
+ await expect(hubToolsService.readResource('hub://servers/NonExistent')).rejects.toThrow('Server not found');
795
1091
  });
796
1092
  it('should return server metadata for server URI with tools field', async () => {
797
1093
  // Arrange
798
1094
  const serverName = 'Test Server';
799
1095
  const mockInstance = {
800
1096
  id: 'test-instance',
1097
+ index: 0,
801
1098
  enabled: true,
802
1099
  args: [],
803
1100
  env: {},
@@ -839,13 +1136,20 @@ describe('HubToolsService', () => {
839
1136
  const mockTools = [
840
1137
  { name: 'testTool', description: 'Test tool description', serverName: 'test-server' }
841
1138
  ];
1139
+ const mockResources = [{ uri: 'test://resource', name: 'Test Resource' }];
842
1140
  // @ts-expect-error - Mocking for test purposes with extra fields
843
1141
  vi.mocked(hubManager.getServerInstancesByName).mockReturnValue([mockInstance]);
844
1142
  vi.mocked(hubManager.getServerByName).mockReturnValue(mockConfig);
845
- vi.mocked(mcpConnectionManager.getTools).mockReturnValue(mockTools);
846
- vi.mocked(mcpConnectionManager.getResources).mockReturnValue([
847
- { uri: 'test://resource', name: 'Test Resource' }
848
- ]);
1143
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
1144
+ vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
1145
+ vi.mocked(mcpConnectionManager.getResourcesByName).mockReturnValue(mockResources);
1146
+ vi.mocked(mcpConnectionManager.getStatus).mockReturnValue({
1147
+ connected: true,
1148
+ lastCheck: mockInstance.lastHeartbeat,
1149
+ startTime: mockInstance.uptime,
1150
+ toolsCount: 1,
1151
+ resourcesCount: 1
1152
+ });
849
1153
  // Act
850
1154
  const result = await hubToolsService.readResource(`hub://servers/${serverName}`);
851
1155
  // Assert
@@ -855,7 +1159,7 @@ describe('HubToolsService', () => {
855
1159
  toolsCount: 1,
856
1160
  tools: { testTool: 'Test tool description' },
857
1161
  resourcesCount: 1,
858
- tags: { env: 'test' },
1162
+ tags: [{ env: 'test' }],
859
1163
  // @ts-expect-error - Accessing extra fields on mock
860
1164
  lastHeartbeat: mockInstance.lastHeartbeat,
861
1165
  // @ts-expect-error - Accessing extra fields on mock
@@ -881,9 +1185,32 @@ describe('HubToolsService', () => {
881
1185
  uptime: 1000
882
1186
  };
883
1187
  const mockTools = [{ name: 'testTool', description: 'Test tool', serverName: 'test-server' }];
1188
+ const mockConfig = {
1189
+ template: {
1190
+ type: 'stdio',
1191
+ command: '',
1192
+ args: [],
1193
+ env: {},
1194
+ headers: {},
1195
+ aggregatedTools: [],
1196
+ timeout: 30000
1197
+ },
1198
+ instances: [mockInstance],
1199
+ tagDefinitions: []
1200
+ };
884
1201
  // @ts-expect-error - Mocking for test purposes with extra fields
885
1202
  vi.mocked(hubManager.getServerInstancesByName).mockReturnValue([mockInstance]);
886
- vi.mocked(mcpConnectionManager.getTools).mockReturnValue(mockTools);
1203
+ // @ts-expect-error - Mock config with simplified mock instance
1204
+ vi.mocked(hubManager.getServerByName).mockReturnValue(mockConfig);
1205
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
1206
+ vi.mocked(mcpConnectionManager.getStatus).mockReturnValue({
1207
+ connected: true,
1208
+ lastCheck: mockInstance.lastHeartbeat,
1209
+ startTime: mockInstance.uptime,
1210
+ toolsCount: 1,
1211
+ resourcesCount: 0
1212
+ });
1213
+ vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
887
1214
  // Act
888
1215
  const result = await hubToolsService.readResource(`hub://servers/${serverName}/tools`);
889
1216
  // Assert
@@ -907,9 +1234,32 @@ describe('HubToolsService', () => {
907
1234
  uptime: 1000
908
1235
  };
909
1236
  const mockResources = [{ uri: 'test://resource', name: 'Test Resource' }];
1237
+ const mockConfig = {
1238
+ template: {
1239
+ type: 'stdio',
1240
+ command: '',
1241
+ args: [],
1242
+ env: {},
1243
+ headers: {},
1244
+ aggregatedTools: [],
1245
+ timeout: 30000
1246
+ },
1247
+ instances: [mockInstance],
1248
+ tagDefinitions: []
1249
+ };
910
1250
  // @ts-expect-error - Mocking for test purposes with extra fields
911
1251
  vi.mocked(hubManager.getServerInstancesByName).mockReturnValue([mockInstance]);
912
- vi.mocked(mcpConnectionManager.getResources).mockReturnValue(mockResources);
1252
+ // @ts-expect-error - Mock config with simplified mock instance
1253
+ vi.mocked(hubManager.getServerByName).mockReturnValue(mockConfig);
1254
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
1255
+ vi.mocked(mcpConnectionManager.getStatus).mockReturnValue({
1256
+ connected: true,
1257
+ lastCheck: mockInstance.lastHeartbeat,
1258
+ startTime: mockInstance.uptime,
1259
+ toolsCount: 0,
1260
+ resourcesCount: 1
1261
+ });
1262
+ vi.mocked(mcpConnectionManager.getResourcesByName).mockReturnValue(mockResources);
913
1263
  // Act
914
1264
  const result = await hubToolsService.readResource(`hub://servers/${serverName}/resources`);
915
1265
  // Assert
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loop_ouroboros/mcp-hub-lite",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "A lightweight MCP management platform designed for independent developers",
5
5
  "license": "MIT",
6
6
  "author": "loop_ouroboros",