@yancyyu/openhermit 1.6.36 → 1.6.38
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/dist-renderer/assets/{ProjectEditorOverlay-B_QAoeaA.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-PB9luAZU.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Dfzcp_Ry.js → _basePickBy-CIhniz70.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-B5u2Yiq2.js → _baseUniq-cKAW4Q8I.js} +1 -1
- package/dist-renderer/assets/{arc-DElOI7qz.js → arc-YmNsoDXW.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Cf6f4tCu.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-FJUdo9Ry.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-BvJQS9lb.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
- package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-n-SGLbin.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-Dwle9tlA.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-Dic8YxQz.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-3n5jC1jk.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-BqizUB3O.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-JRDmD8o9.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-BxFpQw92.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-ByqPwtW9.js → chunk-TZMSLE5B-B0xGXInl.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +1 -0
- package/dist-renderer/assets/clone-BTNuUva-.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CVztr86T.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CIui920O.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-CyL8-bgb.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-CM_67YoY.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DtrtPSGg.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-bOICzF9d.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CJV1g9Hr.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CXbhDo09.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vbXopTpS.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
- package/dist-renderer/assets/{graph-CY2T-j4q.js → graph-DOe5j8dH.js} +1 -1
- package/dist-renderer/assets/{index-Dv7q-OB0.js → index-B2Dy7M2G.js} +1 -1
- package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
- package/dist-renderer/assets/{index-CpyChjme.js → index-BySQS7AB.js} +1 -1
- package/dist-renderer/assets/{index-S-i9egm8.js → index-C_okzZXP.js} +1 -1
- package/dist-renderer/assets/{index-Mrh4pTHw.js → index-CzWxVCRL.js} +1 -1
- package/dist-renderer/assets/{index-Dn-BpzSm.js → index-V7dAKPqd.js} +571 -607
- package/dist-renderer/assets/{index-C9ONRXVI.js → index-VJ-MM9xa.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-BNg14AdU.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-2_PkPCiu.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DjTx7qoU.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
- package/dist-renderer/assets/{layout-DZlHGGN0.js → layout-B-z3y17c.js} +1 -1
- package/dist-renderer/assets/{linear-DnlOm48z.js → linear-D-RTX5UW.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-B-nrgt7V.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BToJFvWR.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C0qoxXvH.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DCKGwsGN.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CS6JCcu7.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-C9pAWoSR.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BTTX_v1m.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BHk97lQJ.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CSWCEzCQ.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CmiIc68g.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-DhwSTphI.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +2 -2
- package/src/main/ipc/extensions.ts +29 -50
- package/src/main/server.ts +17 -26
- package/src/main/services/extensions/ExtensionFacadeService.ts +2 -51
- package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
- package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -7
- package/src/renderer/api/httpClient.ts +108 -22
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -96
- package/src/renderer/components/extensions/plugins/PluginCard.tsx +8 -0
- package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +13 -8
- package/src/renderer/components/team/TeamDetailView.tsx +15 -0
- package/src/renderer/components/team/tools/AddMcpInline.tsx +47 -0
- package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
- package/src/renderer/components/team/tools/McpChip.tsx +42 -0
- package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
- package/src/renderer/components/team/tools/ToolsSection.tsx +208 -0
- package/src/shared/types/extensions/api.ts +9 -0
- package/src/shared/types/extensions/index.ts +4 -0
- package/src/shared/types/extensions/mcp.ts +41 -0
- package/src/shared/utils/extensionNormalizers.ts +22 -0
- package/dist-renderer/assets/channel-DnbgZg0A.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-BAD4p014.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BAD4p014.js +0 -1
- package/dist-renderer/assets/clone-CRX5ZTPd.js +0 -1
- package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
- package/src/main/services/extensions/install/McpInstallService.ts +0 -407
- package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
- package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
- package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
- package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
package/src/main/server.ts
CHANGED
|
@@ -4768,38 +4768,12 @@ app.post('/api/extensions/plugins/uninstall', async (request) => {
|
|
|
4768
4768
|
return result;
|
|
4769
4769
|
});
|
|
4770
4770
|
|
|
4771
|
-
app.get('/api/extensions/mcp/search', async (request) => {
|
|
4772
|
-
const query = (request.query as Record<string, string>).q ?? '';
|
|
4773
|
-
const limit = Number((request.query as Record<string, string>).limit) || 20;
|
|
4774
|
-
const result = await ext.mcpSearch(query, limit);
|
|
4775
|
-
return result;
|
|
4776
|
-
});
|
|
4777
|
-
|
|
4778
|
-
app.get('/api/extensions/mcp/browse', async (request) => {
|
|
4779
|
-
const cursor = (request.query as Record<string, string>).cursor;
|
|
4780
|
-
const limit = Number((request.query as Record<string, string>).limit) || 20;
|
|
4781
|
-
const result = await ext.mcpBrowse(cursor || undefined, limit);
|
|
4782
|
-
return result;
|
|
4783
|
-
});
|
|
4784
|
-
|
|
4785
4771
|
app.get('/api/extensions/mcp/installed', async (request) => {
|
|
4786
4772
|
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4787
4773
|
const result = await ext.mcpGetInstalled(projectPath);
|
|
4788
4774
|
return result;
|
|
4789
4775
|
});
|
|
4790
4776
|
|
|
4791
|
-
app.get('/api/extensions/mcp/:registryId', async (request) => {
|
|
4792
|
-
const { registryId } = request.params as { registryId: string };
|
|
4793
|
-
const result = await ext.mcpGetById(registryId);
|
|
4794
|
-
return result;
|
|
4795
|
-
});
|
|
4796
|
-
|
|
4797
|
-
app.post('/api/extensions/mcp/install', async (request) => {
|
|
4798
|
-
const body = request.body as Record<string, unknown>;
|
|
4799
|
-
const result = await ext.mcpInstall(body as any);
|
|
4800
|
-
return result;
|
|
4801
|
-
});
|
|
4802
|
-
|
|
4803
4777
|
app.post('/api/extensions/mcp/install-custom', async (request) => {
|
|
4804
4778
|
const body = request.body as Record<string, unknown>;
|
|
4805
4779
|
const result = await ext.mcpInstallCustom(body as any);
|
|
@@ -4817,6 +4791,23 @@ app.post('/api/extensions/mcp/uninstall', async (request) => {
|
|
|
4817
4791
|
return result;
|
|
4818
4792
|
});
|
|
4819
4793
|
|
|
4794
|
+
app.get('/api/extensions/mcp/library', async () => {
|
|
4795
|
+
return ext.mcpLibraryList();
|
|
4796
|
+
});
|
|
4797
|
+
|
|
4798
|
+
app.post('/api/extensions/mcp/library', async (request) => {
|
|
4799
|
+
return ext.mcpLibraryUpsert(request.body as any);
|
|
4800
|
+
});
|
|
4801
|
+
|
|
4802
|
+
app.delete('/api/extensions/mcp/library/:id', async (request) => {
|
|
4803
|
+
const { id } = request.params as { id: string };
|
|
4804
|
+
return ext.mcpLibraryDelete(id);
|
|
4805
|
+
});
|
|
4806
|
+
|
|
4807
|
+
app.post('/api/extensions/mcp/library/import', async (request) => {
|
|
4808
|
+
return ext.mcpLibraryImport((request.body ?? {}) as any);
|
|
4809
|
+
});
|
|
4810
|
+
|
|
4820
4811
|
app.get('/api/extensions/skills', async (request) => {
|
|
4821
4812
|
const projectPath = (request.query as Record<string, string>).projectPath;
|
|
4822
4813
|
const result = await ext.skillsList(projectPath);
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Facade service that combines plugin catalog +
|
|
2
|
+
* Facade service that combines plugin catalog + installation state
|
|
3
3
|
* into enriched data ready for the renderer.
|
|
4
4
|
*
|
|
5
5
|
* Also provides install target resolution for the security model
|
|
6
|
-
* (main-side re-resolution: renderer sends pluginId
|
|
6
|
+
* (main-side re-resolution: renderer sends pluginId, main resolves from catalog).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createLogger } from '@shared/utils/logger';
|
|
10
10
|
|
|
11
|
-
import { type McpCatalogAggregator } from './catalog/McpCatalogAggregator';
|
|
12
11
|
import { type PluginCatalogService } from './catalog/PluginCatalogService';
|
|
13
|
-
import { type McpInstallationStateService } from './state/McpInstallationStateService';
|
|
14
12
|
import { type PluginInstallationStateService } from './state/PluginInstallationStateService';
|
|
15
13
|
|
|
16
14
|
import type {
|
|
17
15
|
EnrichedPlugin,
|
|
18
|
-
InstalledMcpEntry,
|
|
19
|
-
McpCatalogItem,
|
|
20
|
-
McpSearchResult,
|
|
21
16
|
PluginCatalogItem,
|
|
22
17
|
} from '@shared/types/extensions';
|
|
23
18
|
|
|
@@ -27,8 +22,6 @@ export class ExtensionFacadeService {
|
|
|
27
22
|
constructor(
|
|
28
23
|
private readonly pluginCatalog: PluginCatalogService,
|
|
29
24
|
private readonly pluginState: PluginInstallationStateService,
|
|
30
|
-
private readonly mcpAggregator: McpCatalogAggregator | null = null,
|
|
31
|
-
private readonly mcpState: McpInstallationStateService | null = null
|
|
32
25
|
) {}
|
|
33
26
|
|
|
34
27
|
// ── Plugin methods ───────────────────────────────────────────────────
|
|
@@ -85,51 +78,9 @@ export class ExtensionFacadeService {
|
|
|
85
78
|
return { qualifiedName: plugin.qualifiedName, plugin };
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
// ── MCP methods ──────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Search MCP servers across both registries.
|
|
92
|
-
*/
|
|
93
|
-
async searchMcp(query: string, limit?: number): Promise<McpSearchResult> {
|
|
94
|
-
if (!this.mcpAggregator) {
|
|
95
|
-
return { servers: [], warnings: ['MCP catalog not configured'] };
|
|
96
|
-
}
|
|
97
|
-
return this.mcpAggregator.search(query, limit);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Browse MCP catalog with pagination.
|
|
102
|
-
*/
|
|
103
|
-
async browseMcp(
|
|
104
|
-
cursor?: string,
|
|
105
|
-
limit?: number
|
|
106
|
-
): Promise<{ servers: McpCatalogItem[]; nextCursor?: string }> {
|
|
107
|
-
if (!this.mcpAggregator) {
|
|
108
|
-
return { servers: [] };
|
|
109
|
-
}
|
|
110
|
-
return this.mcpAggregator.browse(cursor, limit);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get a single MCP server by registry ID (for install flow).
|
|
115
|
-
*/
|
|
116
|
-
async getMcpById(registryId: string): Promise<McpCatalogItem | null> {
|
|
117
|
-
if (!this.mcpAggregator) return null;
|
|
118
|
-
return this.mcpAggregator.getById(registryId);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get installed MCP servers.
|
|
123
|
-
*/
|
|
124
|
-
async getInstalledMcp(projectPath?: string): Promise<InstalledMcpEntry[]> {
|
|
125
|
-
if (!this.mcpState) return [];
|
|
126
|
-
return this.mcpState.getInstalled(projectPath);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
81
|
// ── Cache invalidation ───────────────────────────────────────────────
|
|
130
82
|
|
|
131
83
|
invalidateInstalledCache(): void {
|
|
132
84
|
this.pluginState.invalidateCache();
|
|
133
|
-
this.mcpState?.invalidateCache();
|
|
134
85
|
}
|
|
135
86
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpLibraryService — a reusable, global library of MCP server definitions.
|
|
3
|
+
*
|
|
4
|
+
* cc-switch model: a server is defined once and can be enabled for any worker
|
|
5
|
+
* (= installed into that worker's project config) without re-entering the
|
|
6
|
+
* command / URL / env each time. The "enable/disable for a worker" action is
|
|
7
|
+
* handled by the existing install/uninstall path; this service only owns the
|
|
8
|
+
* persisted library of definitions.
|
|
9
|
+
*
|
|
10
|
+
* Storage: ~/.hermit/mcp-library.json (HERMIT_HOME override respected).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
import { createLogger } from '@shared/utils/logger';
|
|
19
|
+
import { getErrorMessage } from '@shared/utils/errorHandling';
|
|
20
|
+
|
|
21
|
+
import { McpConfigStateReader } from '../runtime/McpConfigStateReader';
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
McpHeaderDef,
|
|
25
|
+
McpInstallSpec,
|
|
26
|
+
McpLibraryEntry,
|
|
27
|
+
McpLibraryImportRequest,
|
|
28
|
+
McpLibraryImportResult,
|
|
29
|
+
McpLibraryUpsertRequest,
|
|
30
|
+
} from '@shared/types/extensions';
|
|
31
|
+
|
|
32
|
+
const logger = createLogger('Extensions:McpLibrary');
|
|
33
|
+
|
|
34
|
+
function getHermitHome(): string {
|
|
35
|
+
return process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Best-effort mapping of a raw MCP server config (from ~/.claude.json /
|
|
40
|
+
* .mcp.json) into the install spec the current install path understands.
|
|
41
|
+
* Returns null when the config cannot be represented (e.g. an arbitrary stdio
|
|
42
|
+
* command rather than an npm package) — the caller skips those.
|
|
43
|
+
*/
|
|
44
|
+
function rawConfigToInstallSpec(config: Record<string, unknown>): McpInstallSpec | null {
|
|
45
|
+
const url = typeof config.url === 'string' ? config.url : null;
|
|
46
|
+
if (url) {
|
|
47
|
+
const rawType = typeof config.type === 'string' ? config.type : 'http';
|
|
48
|
+
const transportType =
|
|
49
|
+
rawType === 'sse' ? 'sse' : rawType === 'streamable-http' ? 'streamable-http' : 'http';
|
|
50
|
+
return { type: 'http', url, transportType };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = typeof config.command === 'string' ? config.command : null;
|
|
54
|
+
if (command) {
|
|
55
|
+
const args = Array.isArray(config.args) ? config.args.map(String) : [];
|
|
56
|
+
const npmPackage = extractNpmPackage(command, args);
|
|
57
|
+
if (npmPackage) {
|
|
58
|
+
return { type: 'stdio', npmPackage };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pull the package spec out of `npx -y <pkg>` / `npm exec <pkg>` style commands. */
|
|
66
|
+
function extractNpmPackage(command: string, args: string[]): string | null {
|
|
67
|
+
const base = path.basename(command);
|
|
68
|
+
if (base !== 'npx' && base !== 'npm' && base !== 'pnpm' && base !== 'bunx') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Skip flags (-y, --yes, exec, ...) and take the first package-looking token.
|
|
72
|
+
for (const arg of args) {
|
|
73
|
+
if (arg.startsWith('-')) continue;
|
|
74
|
+
if (arg === 'exec' || arg === 'dlx') continue;
|
|
75
|
+
return arg;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractEnvValues(config: Record<string, unknown>): Record<string, string> | undefined {
|
|
81
|
+
const env = config.env;
|
|
82
|
+
if (!env || typeof env !== 'object' || Array.isArray(env)) return undefined;
|
|
83
|
+
const out: Record<string, string> = {};
|
|
84
|
+
for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
|
|
85
|
+
if (typeof value === 'string') out[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractHeaders(config: Record<string, unknown>): McpHeaderDef[] | undefined {
|
|
91
|
+
const headers = config.headers;
|
|
92
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return undefined;
|
|
93
|
+
const out: McpHeaderDef[] = [];
|
|
94
|
+
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
95
|
+
if (typeof value === 'string') out.push({ key, value });
|
|
96
|
+
}
|
|
97
|
+
return out.length > 0 ? out : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class McpLibraryService {
|
|
101
|
+
private readonly filePath: string;
|
|
102
|
+
private entries: McpLibraryEntry[] | null = null;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
dataDir: string = getHermitHome(),
|
|
106
|
+
private readonly stateReader = new McpConfigStateReader()
|
|
107
|
+
) {
|
|
108
|
+
this.filePath = path.join(dataDir, 'mcp-library.json');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Read ─────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
list(): McpLibraryEntry[] {
|
|
114
|
+
return [...this.load()].sort((a, b) => a.name.localeCompare(b.name));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Write ──────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
upsert(request: McpLibraryUpsertRequest): McpLibraryEntry {
|
|
120
|
+
const name = request.name.trim();
|
|
121
|
+
if (!name) throw new Error('MCP 名称不能为空');
|
|
122
|
+
|
|
123
|
+
const entries = this.load();
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
|
|
126
|
+
if (request.id) {
|
|
127
|
+
const existing = entries.find((e) => e.id === request.id);
|
|
128
|
+
if (!existing) throw new Error(`未找到库条目: ${request.id}`);
|
|
129
|
+
this.assertNameAvailable(entries, name, request.id);
|
|
130
|
+
existing.name = name;
|
|
131
|
+
existing.description = request.description?.trim() || undefined;
|
|
132
|
+
existing.installSpec = request.installSpec;
|
|
133
|
+
existing.envValues = request.envValues;
|
|
134
|
+
existing.headers = request.headers;
|
|
135
|
+
existing.updatedAt = now;
|
|
136
|
+
this.save(entries);
|
|
137
|
+
return existing;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.assertNameAvailable(entries, name, null);
|
|
141
|
+
const entry: McpLibraryEntry = {
|
|
142
|
+
id: randomUUID(),
|
|
143
|
+
name,
|
|
144
|
+
description: request.description?.trim() || undefined,
|
|
145
|
+
installSpec: request.installSpec,
|
|
146
|
+
envValues: request.envValues,
|
|
147
|
+
headers: request.headers,
|
|
148
|
+
createdAt: now,
|
|
149
|
+
updatedAt: now,
|
|
150
|
+
};
|
|
151
|
+
entries.push(entry);
|
|
152
|
+
this.save(entries);
|
|
153
|
+
return entry;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
remove(id: string): void {
|
|
157
|
+
const entries = this.load();
|
|
158
|
+
const next = entries.filter((e) => e.id !== id);
|
|
159
|
+
if (next.length !== entries.length) this.save(next);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Import MCP servers already present in live config into the library.
|
|
164
|
+
* Skips entries whose name already exists, and ones whose config can't be
|
|
165
|
+
* represented by the current install spec (e.g. arbitrary stdio commands).
|
|
166
|
+
*/
|
|
167
|
+
async importFromLive(request: McpLibraryImportRequest): Promise<McpLibraryImportResult> {
|
|
168
|
+
const configured = await this.stateReader.readConfigured(request.projectPath);
|
|
169
|
+
const entries = this.load();
|
|
170
|
+
const existingNames = new Set(entries.map((e) => e.name.toLowerCase()));
|
|
171
|
+
|
|
172
|
+
const imported: string[] = [];
|
|
173
|
+
const skipped: string[] = [];
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
|
|
176
|
+
for (const entry of configured) {
|
|
177
|
+
if (existingNames.has(entry.name.toLowerCase())) {
|
|
178
|
+
skipped.push(entry.name);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const installSpec = rawConfigToInstallSpec(entry.config);
|
|
182
|
+
if (!installSpec) {
|
|
183
|
+
skipped.push(entry.name);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
entries.push({
|
|
187
|
+
id: randomUUID(),
|
|
188
|
+
name: entry.name,
|
|
189
|
+
installSpec,
|
|
190
|
+
envValues: extractEnvValues(entry.config),
|
|
191
|
+
headers: extractHeaders(entry.config),
|
|
192
|
+
createdAt: now,
|
|
193
|
+
updatedAt: now,
|
|
194
|
+
});
|
|
195
|
+
existingNames.add(entry.name.toLowerCase());
|
|
196
|
+
imported.push(entry.name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (imported.length > 0) this.save(entries);
|
|
200
|
+
return { imported, skipped };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Private ──────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
private assertNameAvailable(
|
|
206
|
+
entries: McpLibraryEntry[],
|
|
207
|
+
name: string,
|
|
208
|
+
ignoreId: string | null
|
|
209
|
+
): void {
|
|
210
|
+
const clash = entries.find(
|
|
211
|
+
(e) => e.name.toLowerCase() === name.toLowerCase() && e.id !== ignoreId
|
|
212
|
+
);
|
|
213
|
+
if (clash) throw new Error(`库中已存在同名 MCP: ${name}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private load(): McpLibraryEntry[] {
|
|
217
|
+
if (this.entries) return this.entries;
|
|
218
|
+
try {
|
|
219
|
+
if (fs.existsSync(this.filePath)) {
|
|
220
|
+
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
221
|
+
const parsed = JSON.parse(raw) as McpLibraryEntry[];
|
|
222
|
+
this.entries = Array.isArray(parsed) ? parsed : [];
|
|
223
|
+
} else {
|
|
224
|
+
this.entries = [];
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.warn(`Failed to load MCP library from ${this.filePath}: ${getErrorMessage(error)}`);
|
|
228
|
+
this.entries = [];
|
|
229
|
+
}
|
|
230
|
+
return this.entries;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private save(entries: McpLibraryEntry[]): void {
|
|
234
|
+
this.entries = entries;
|
|
235
|
+
try {
|
|
236
|
+
const dir = path.dirname(this.filePath);
|
|
237
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(this.filePath, JSON.stringify(entries, null, 2), 'utf-8');
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logger.warn(`Failed to save MCP library to ${this.filePath}: ${getErrorMessage(error)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -26,6 +26,8 @@ function redisConfig(cfg: TaskBusConfig) {
|
|
|
26
26
|
password: cfg.redis.password,
|
|
27
27
|
db: cfg.redis.db,
|
|
28
28
|
lazyConnect: true,
|
|
29
|
+
maxRetriesPerRequest: 0,
|
|
30
|
+
retryStrategy: () => null,
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -39,6 +41,9 @@ async function getRedis(cfg: TaskBusConfig): Promise<Redis | null> {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const r = new Redis(redisConfig(cfg));
|
|
44
|
+
r.on('error', () => {
|
|
45
|
+
/* handled by connect/ping fallback */
|
|
46
|
+
});
|
|
42
47
|
try {
|
|
43
48
|
await r.connect();
|
|
44
49
|
await r.ping();
|
|
@@ -107,7 +112,7 @@ async function doScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
|
|
|
107
112
|
const result = await scanSessions();
|
|
108
113
|
lastLocalScan = statusFromParseResult(result, false);
|
|
109
114
|
|
|
110
|
-
if (!cfg.telemetry.uploadEnabled) {
|
|
115
|
+
if (!cfg.enabled || !cfg.telemetry.uploadEnabled) {
|
|
111
116
|
return result;
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -212,10 +217,18 @@ export async function getTelemetryStatus(
|
|
|
212
217
|
|
|
213
218
|
const cfg = { redis: redisCfg };
|
|
214
219
|
const client = new Redis(redisConfig(cfg as TaskBusConfig));
|
|
220
|
+
client.on('error', () => {
|
|
221
|
+
/* handled by connect/ping fallback */
|
|
222
|
+
});
|
|
215
223
|
try {
|
|
216
224
|
await client.connect();
|
|
217
225
|
await client.ping();
|
|
218
226
|
} catch {
|
|
227
|
+
try {
|
|
228
|
+
client.disconnect();
|
|
229
|
+
} catch {
|
|
230
|
+
/* ignore */
|
|
231
|
+
}
|
|
219
232
|
return lastLocalScan;
|
|
220
233
|
}
|
|
221
234
|
|
|
@@ -79,6 +79,7 @@ export class TaskDispatchService {
|
|
|
79
79
|
this.stopHeartbeat();
|
|
80
80
|
this.stopConsumers();
|
|
81
81
|
this.stopResponseConsumers();
|
|
82
|
+
this.collabBoard.setRedis(null);
|
|
82
83
|
this.redis?.disconnect();
|
|
83
84
|
this.redisSub?.disconnect();
|
|
84
85
|
this.redis = null;
|
|
@@ -757,27 +758,51 @@ export class TaskDispatchService {
|
|
|
757
758
|
|
|
758
759
|
private async connectRedis(): Promise<void> {
|
|
759
760
|
if (!this.config?.redis) return;
|
|
761
|
+
let redis: Redis | null = null;
|
|
762
|
+
let redisSub: Redis | null = null;
|
|
760
763
|
try {
|
|
761
764
|
const ioredis = await import('ioredis');
|
|
762
765
|
const { host, port, password, db } = this.config.redis;
|
|
763
|
-
const opts = {
|
|
766
|
+
const opts = {
|
|
767
|
+
host,
|
|
768
|
+
port,
|
|
769
|
+
password: password || undefined,
|
|
770
|
+
db: db ?? 0,
|
|
771
|
+
lazyConnect: true,
|
|
772
|
+
maxRetriesPerRequest: 0,
|
|
773
|
+
retryStrategy: () => null,
|
|
774
|
+
};
|
|
764
775
|
|
|
765
|
-
|
|
766
|
-
|
|
776
|
+
redis = new ioredis.default(opts);
|
|
777
|
+
redisSub = new ioredis.default(opts);
|
|
767
778
|
|
|
768
|
-
|
|
769
|
-
console.
|
|
779
|
+
redis.on('error', (err: Error) => {
|
|
780
|
+
console.warn('[TaskDispatchService] Redis error:', err.message);
|
|
781
|
+
});
|
|
782
|
+
redisSub.on('error', (err: Error) => {
|
|
783
|
+
console.warn('[TaskDispatchService] Redis subscriber error:', err.message);
|
|
770
784
|
});
|
|
771
785
|
|
|
772
|
-
await
|
|
786
|
+
await redis.connect();
|
|
787
|
+
await redisSub.connect();
|
|
788
|
+
await redis.ping();
|
|
773
789
|
|
|
790
|
+
this.redis = redis;
|
|
791
|
+
this.redisSub = redisSub;
|
|
792
|
+
redis = null;
|
|
793
|
+
redisSub = null;
|
|
774
794
|
this.collabBoard.setRedis(this.redis);
|
|
775
795
|
this.startHeartbeat();
|
|
776
796
|
this.startConsumers();
|
|
777
797
|
this.startResponseConsumers();
|
|
778
798
|
this.subscribeStatus();
|
|
779
799
|
} catch (err) {
|
|
780
|
-
console.
|
|
800
|
+
console.warn('[TaskDispatchService] Redis connect failed:', err);
|
|
801
|
+
this.collabBoard.setRedis(null);
|
|
802
|
+
redis?.disconnect();
|
|
803
|
+
redisSub?.disconnect();
|
|
804
|
+
this.redis?.disconnect();
|
|
805
|
+
this.redisSub?.disconnect();
|
|
781
806
|
this.redis = null;
|
|
782
807
|
this.redisSub = null;
|
|
783
808
|
}
|