@yancyyu/openhermit 1.6.37 → 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-Va_Vz-zz.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-DYT3bwFR.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Dbt_EU-e.js → _basePickBy-CIhniz70.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DWo68sXI.js → _baseUniq-cKAW4Q8I.js} +1 -1
- package/dist-renderer/assets/{arc-DXH1iZQK.js → arc-YmNsoDXW.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-cjffS2Qr.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BKdZF02Y.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CN27pqaI.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
- package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CXPCI7g_.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-BGAXQZRC.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-TPDaA_KQ.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-D1ADe_tq.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Beimtg3a.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-OjNBu854.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DinqvbH8.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BfFtlPSZ.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-D9z9Dgt7.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-n1g-DhEE.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BvxFq-BE.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-wVnJuwza.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-B707WJQw.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-C-_1dGHs.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CMTSi3H6.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DZ0bNrAA.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DNVfGooQ.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
- package/dist-renderer/assets/{graph-865j_tM_.js → graph-DOe5j8dH.js} +1 -1
- package/dist-renderer/assets/{index-2EW-eu3q.js → index-B2Dy7M2G.js} +1 -1
- package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
- package/dist-renderer/assets/{index-C_F9N5x-.js → index-BySQS7AB.js} +1 -1
- package/dist-renderer/assets/{index-4dEMStJj.js → index-C_okzZXP.js} +1 -1
- package/dist-renderer/assets/{index-DuUaf8at.js → index-CzWxVCRL.js} +1 -1
- package/dist-renderer/assets/{index-LwDIsXJN.js → index-V7dAKPqd.js} +571 -607
- package/dist-renderer/assets/{index-BTx1nc4T.js → index-VJ-MM9xa.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-CyqtElLq.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BvjQ0Hm0.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CJJ-k0zT.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
- package/dist-renderer/assets/{layout-CnV6rQAG.js → layout-B-z3y17c.js} +1 -1
- package/dist-renderer/assets/{linear-Cw3UQgyX.js → linear-D-RTX5UW.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-C5tDaGSK.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-CiIpPsau.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C3gtowNj.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CXBTrAnU.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-wziX77xG.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-sYqopcrj.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bl1-0_Cp.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-DOYYvDbi.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CIRjJUBo.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVPuNe1n.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-3nT9yHwp.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- 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-5dJIx68e.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-BMGXWJ2d.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BMGXWJ2d.js +0 -1
- package/dist-renderer/assets/clone-D7FWfGY9.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
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* McpInstallService — installs/uninstalls MCP servers via Claude CLI.
|
|
3
|
-
*
|
|
4
|
-
* Security model: renderer sends ONLY registryId + user inputs (env values,
|
|
5
|
-
* headers, server name). Main re-fetches server spec from registry via getById()
|
|
6
|
-
* and builds CLI args from the fresh registry data (never trusts install spec
|
|
7
|
-
* from renderer).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
|
11
|
-
import { execCli } from '@main/utils/childProcess';
|
|
12
|
-
import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli';
|
|
13
|
-
import { createLogger } from '@shared/utils/logger';
|
|
14
|
-
import { isProjectScopedMcpScope } from '@shared/utils/mcpScopes';
|
|
15
|
-
import path from 'path';
|
|
16
|
-
|
|
17
|
-
import { createExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
|
|
18
|
-
|
|
19
|
-
import type { McpCatalogAggregator } from '../catalog/McpCatalogAggregator';
|
|
20
|
-
import type { ExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
|
|
21
|
-
import type {
|
|
22
|
-
McpCustomInstallRequest,
|
|
23
|
-
McpInstallRequest,
|
|
24
|
-
OperationResult,
|
|
25
|
-
} from '@shared/types/extensions';
|
|
26
|
-
|
|
27
|
-
const logger = createLogger('Extensions:McpInstall');
|
|
28
|
-
|
|
29
|
-
/** Validate server name: alphanumeric, dashes, underscores, dots */
|
|
30
|
-
const SERVER_NAME_RE = /^[\w.-]{1,100}$/;
|
|
31
|
-
|
|
32
|
-
/** Allowed scope values (prevent command injection) */
|
|
33
|
-
const VALID_SCOPES = new Set(['local', 'user', 'project', 'global']);
|
|
34
|
-
|
|
35
|
-
/** Env var key must be safe shell identifier */
|
|
36
|
-
const ENV_KEY_RE = /^[A-Z_][A-Z0-9_]{0,100}$/i;
|
|
37
|
-
|
|
38
|
-
/** HTTP header key must be safe (RFC 7230 token) */
|
|
39
|
-
const HEADER_KEY_RE = /^[A-Za-z][\w-]{0,100}$/;
|
|
40
|
-
|
|
41
|
-
const TIMEOUT_MS = 30_000;
|
|
42
|
-
|
|
43
|
-
function scopeRequiresProjectPath(scope?: string): boolean {
|
|
44
|
-
return isProjectScopedMcpScope(scope);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class McpInstallService {
|
|
48
|
-
constructor(
|
|
49
|
-
private readonly aggregator: McpCatalogAggregator,
|
|
50
|
-
private readonly runtimeAdapter: ExtensionsRuntimeAdapter = createExtensionsRuntimeAdapter()
|
|
51
|
-
) {}
|
|
52
|
-
|
|
53
|
-
async install(request: McpInstallRequest): Promise<OperationResult> {
|
|
54
|
-
const { registryId, serverName, scope, projectPath, envValues, headers } = request;
|
|
55
|
-
|
|
56
|
-
// 1. Validate server name
|
|
57
|
-
if (!SERVER_NAME_RE.test(serverName)) {
|
|
58
|
-
return {
|
|
59
|
-
state: 'error',
|
|
60
|
-
error: `Invalid server name: "${serverName}". Use alphanumeric, dashes, underscores, dots.`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 2. Validate scope
|
|
65
|
-
if (scope && !VALID_SCOPES.has(scope)) {
|
|
66
|
-
return {
|
|
67
|
-
state: 'error',
|
|
68
|
-
error: `Invalid scope: "${scope}". Must be one of: local, user, project, global.`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (scopeRequiresProjectPath(scope) && !projectPath) {
|
|
73
|
-
return {
|
|
74
|
-
state: 'error',
|
|
75
|
-
error: `projectPath is required for ${scope} scope`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 3. Validate env var keys (prevent command injection)
|
|
80
|
-
for (const key of Object.keys(envValues)) {
|
|
81
|
-
if (!ENV_KEY_RE.test(key)) {
|
|
82
|
-
return {
|
|
83
|
-
state: 'error',
|
|
84
|
-
error: `Invalid environment variable name: "${key}". Use uppercase alphanumeric and underscores.`,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 4. Validate header keys (prevent header injection)
|
|
90
|
-
for (const header of headers) {
|
|
91
|
-
if (header.key && !HEADER_KEY_RE.test(header.key)) {
|
|
92
|
-
return {
|
|
93
|
-
state: 'error',
|
|
94
|
-
error: `Invalid header name: "${header.key}". Use alphanumeric, dashes, underscores.`,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 5. Validate projectPath (if provided, must be absolute)
|
|
100
|
-
if (projectPath && !path.isAbsolute(projectPath)) {
|
|
101
|
-
return {
|
|
102
|
-
state: 'error',
|
|
103
|
-
error: 'projectPath must be an absolute path',
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 6. Re-fetch from registry (don't trust renderer-provided install spec)
|
|
108
|
-
const server = await this.aggregator.getById(registryId);
|
|
109
|
-
if (!server) {
|
|
110
|
-
return {
|
|
111
|
-
state: 'error',
|
|
112
|
-
error: `MCP server "${registryId}" not found in registry`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!server.installSpec) {
|
|
117
|
-
return {
|
|
118
|
-
state: 'error',
|
|
119
|
-
error: `MCP server "${server.name}" does not have an automatic install spec. Manual setup required.`,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 7. Build CLI args based on install spec type
|
|
124
|
-
const args: string[] = ['mcp', 'add'];
|
|
125
|
-
|
|
126
|
-
// Scope flag (-s)
|
|
127
|
-
if (scope && scope !== 'local') {
|
|
128
|
-
args.push('-s', scope);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (server.installSpec.type === 'stdio') {
|
|
132
|
-
// Stdio: claude mcp add [-s scope] [-e KEY=val...] <name> -- npx -y <package>[@version]
|
|
133
|
-
// Add env flags
|
|
134
|
-
for (const [key, value] of Object.entries(envValues)) {
|
|
135
|
-
if (key && value) {
|
|
136
|
-
args.push('-e', `${key}=${value}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
args.push(serverName);
|
|
141
|
-
args.push('--');
|
|
142
|
-
args.push('npx', '-y');
|
|
143
|
-
|
|
144
|
-
const pkg = server.installSpec.npmVersion
|
|
145
|
-
? `${server.installSpec.npmPackage}@${server.installSpec.npmVersion}`
|
|
146
|
-
: server.installSpec.npmPackage;
|
|
147
|
-
args.push(pkg);
|
|
148
|
-
} else if (server.installSpec.type === 'http') {
|
|
149
|
-
// HTTP/SSE: claude mcp add [-s scope] -t <transport> [-H "Key: val"...] <name> <url>
|
|
150
|
-
const transport = server.installSpec.transportType === 'sse' ? 'sse' : 'http';
|
|
151
|
-
args.push('-t', transport);
|
|
152
|
-
|
|
153
|
-
// Add header flags
|
|
154
|
-
for (const header of headers) {
|
|
155
|
-
if (header.key && header.value) {
|
|
156
|
-
args.push('-H', `${header.key}:${header.value}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Add env flags (some HTTP servers also need env vars)
|
|
161
|
-
for (const [key, value] of Object.entries(envValues)) {
|
|
162
|
-
if (key && value) {
|
|
163
|
-
args.push('-e', `${key}=${value}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
args.push(serverName);
|
|
168
|
-
args.push(server.installSpec.url);
|
|
169
|
-
} else {
|
|
170
|
-
return {
|
|
171
|
-
state: 'error',
|
|
172
|
-
error: `Unsupported install spec type: ${(server.installSpec as { type: string }).type}`,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
logger.info(
|
|
177
|
-
`Installing MCP server: ${serverName} (type: ${server.installSpec.type}, scope: ${scope ?? 'local'})`
|
|
178
|
-
);
|
|
179
|
-
// Don't log env values or header values (may contain secrets)
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
const claudeBinary = await ClaudeBinaryResolver.resolve();
|
|
183
|
-
if (!claudeBinary) {
|
|
184
|
-
return {
|
|
185
|
-
state: 'error',
|
|
186
|
-
error: CLI_NOT_FOUND_MESSAGE,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
|
|
190
|
-
|
|
191
|
-
const { stderr } = await execCli(claudeBinary, args, {
|
|
192
|
-
timeout: TIMEOUT_MS,
|
|
193
|
-
cwd: projectPath,
|
|
194
|
-
env,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
if (stderr) {
|
|
198
|
-
logger.warn(`MCP install stderr: ${stderr.slice(0, 200)}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return { state: 'success' };
|
|
202
|
-
} catch (err) {
|
|
203
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
-
// Mask potential secrets in error output
|
|
205
|
-
const safeMessage = maskSecrets(
|
|
206
|
-
message,
|
|
207
|
-
envValues,
|
|
208
|
-
headers.map((h) => h.value)
|
|
209
|
-
);
|
|
210
|
-
logger.error(`MCP install failed: ${safeMessage}`);
|
|
211
|
-
return { state: 'error', error: safeMessage };
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Install a custom MCP server — user provides installSpec directly (bypasses registry).
|
|
217
|
-
*/
|
|
218
|
-
async installCustom(request: McpCustomInstallRequest): Promise<OperationResult> {
|
|
219
|
-
const { serverName, scope, projectPath, installSpec, envValues, headers } = request;
|
|
220
|
-
|
|
221
|
-
// Validate inputs (same rules as registry install)
|
|
222
|
-
if (!SERVER_NAME_RE.test(serverName)) {
|
|
223
|
-
return {
|
|
224
|
-
state: 'error',
|
|
225
|
-
error: `Invalid server name: "${serverName}". Use alphanumeric, dashes, underscores, dots.`,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (scope && !VALID_SCOPES.has(scope)) {
|
|
230
|
-
return { state: 'error', error: `Invalid scope: "${scope}".` };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (scopeRequiresProjectPath(scope) && !projectPath) {
|
|
234
|
-
return { state: 'error', error: `projectPath is required for ${scope} scope` };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
for (const key of Object.keys(envValues)) {
|
|
238
|
-
if (!ENV_KEY_RE.test(key)) {
|
|
239
|
-
return { state: 'error', error: `Invalid env var name: "${key}".` };
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (const header of headers) {
|
|
244
|
-
if (header.key && !HEADER_KEY_RE.test(header.key)) {
|
|
245
|
-
return { state: 'error', error: `Invalid header name: "${header.key}".` };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (projectPath && !path.isAbsolute(projectPath)) {
|
|
250
|
-
return { state: 'error', error: 'projectPath must be an absolute path' };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Build CLI args from provided installSpec
|
|
254
|
-
const args: string[] = ['mcp', 'add'];
|
|
255
|
-
|
|
256
|
-
if (scope && scope !== 'local') {
|
|
257
|
-
args.push('-s', scope);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (installSpec.type === 'stdio') {
|
|
261
|
-
for (const [key, value] of Object.entries(envValues)) {
|
|
262
|
-
if (key && value) args.push('-e', `${key}=${value}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
args.push(serverName);
|
|
266
|
-
args.push('--');
|
|
267
|
-
args.push('npx', '-y');
|
|
268
|
-
|
|
269
|
-
const pkg = installSpec.npmVersion
|
|
270
|
-
? `${installSpec.npmPackage}@${installSpec.npmVersion}`
|
|
271
|
-
: installSpec.npmPackage;
|
|
272
|
-
args.push(pkg);
|
|
273
|
-
} else if (installSpec.type === 'http') {
|
|
274
|
-
const transport = installSpec.transportType === 'sse' ? 'sse' : 'http';
|
|
275
|
-
args.push('-t', transport);
|
|
276
|
-
|
|
277
|
-
// Positional args must come before variadic flags (-H, -e)
|
|
278
|
-
args.push(serverName);
|
|
279
|
-
args.push(installSpec.url);
|
|
280
|
-
|
|
281
|
-
for (const header of headers) {
|
|
282
|
-
if (header.key && header.value) {
|
|
283
|
-
args.push('-H', `${header.key}:${header.value}`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
for (const [key, value] of Object.entries(envValues)) {
|
|
288
|
-
if (key && value) args.push('-e', `${key}=${value}`);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
return { state: 'error', error: 'Unsupported install spec type' };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
logger.info(
|
|
295
|
-
`Installing custom MCP server: ${serverName} (type: ${installSpec.type}, scope: ${scope ?? 'local'})`
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
const claudeBinary = await ClaudeBinaryResolver.resolve();
|
|
300
|
-
if (!claudeBinary) {
|
|
301
|
-
return {
|
|
302
|
-
state: 'error',
|
|
303
|
-
error: CLI_NOT_FOUND_MESSAGE,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
|
|
307
|
-
|
|
308
|
-
const { stderr } = await execCli(claudeBinary, args, {
|
|
309
|
-
timeout: TIMEOUT_MS,
|
|
310
|
-
cwd: projectPath,
|
|
311
|
-
env,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
if (stderr) {
|
|
315
|
-
logger.warn(`Custom MCP install stderr: ${stderr.slice(0, 200)}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return { state: 'success' };
|
|
319
|
-
} catch (err) {
|
|
320
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
-
const safeMessage = maskSecrets(
|
|
322
|
-
message,
|
|
323
|
-
envValues,
|
|
324
|
-
headers.map((h) => h.value)
|
|
325
|
-
);
|
|
326
|
-
logger.error(`Custom MCP install failed: ${safeMessage}`);
|
|
327
|
-
return { state: 'error', error: safeMessage };
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async uninstall(name: string, scope?: string, projectPath?: string): Promise<OperationResult> {
|
|
332
|
-
if (!SERVER_NAME_RE.test(name)) {
|
|
333
|
-
return {
|
|
334
|
-
state: 'error',
|
|
335
|
-
error: `Invalid server name: "${name}"`,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (scope && !VALID_SCOPES.has(scope)) {
|
|
340
|
-
return {
|
|
341
|
-
state: 'error',
|
|
342
|
-
error: `Invalid scope: "${scope}". Must be one of: local, user, project, global.`,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (scopeRequiresProjectPath(scope) && !projectPath) {
|
|
347
|
-
return {
|
|
348
|
-
state: 'error',
|
|
349
|
-
error: `projectPath is required for ${scope} scope`,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (projectPath && !path.isAbsolute(projectPath)) {
|
|
354
|
-
return {
|
|
355
|
-
state: 'error',
|
|
356
|
-
error: 'projectPath must be an absolute path',
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const args = ['mcp', 'remove'];
|
|
361
|
-
if (scope && scope !== 'local') {
|
|
362
|
-
args.push('-s', scope);
|
|
363
|
-
}
|
|
364
|
-
args.push(name);
|
|
365
|
-
|
|
366
|
-
logger.info(`Removing MCP server: ${name} (scope: ${scope ?? 'local'})`);
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const claudeBinary = await ClaudeBinaryResolver.resolve();
|
|
370
|
-
if (!claudeBinary) {
|
|
371
|
-
return {
|
|
372
|
-
state: 'error',
|
|
373
|
-
error: CLI_NOT_FOUND_MESSAGE,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
|
|
377
|
-
|
|
378
|
-
await execCli(claudeBinary, args, {
|
|
379
|
-
timeout: TIMEOUT_MS,
|
|
380
|
-
cwd: projectPath,
|
|
381
|
-
env,
|
|
382
|
-
});
|
|
383
|
-
return { state: 'success' };
|
|
384
|
-
} catch (err) {
|
|
385
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
386
|
-
logger.error(`MCP uninstall failed: ${message}`);
|
|
387
|
-
return { state: 'error', error: message };
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Replace secret values in error messages with [REDACTED] */
|
|
393
|
-
function maskSecrets(
|
|
394
|
-
message: string,
|
|
395
|
-
envValues: Record<string, string>,
|
|
396
|
-
headerValues: string[]
|
|
397
|
-
): string {
|
|
398
|
-
let result = message;
|
|
399
|
-
const secrets = [
|
|
400
|
-
...Object.values(envValues).filter((v) => v.length > 3),
|
|
401
|
-
...headerValues.filter((v) => v.length > 3),
|
|
402
|
-
];
|
|
403
|
-
for (const secret of secrets) {
|
|
404
|
-
result = result.replaceAll(secret, '[REDACTED]');
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves installed MCP server state through the active runtime adapter.
|
|
3
|
-
*
|
|
4
|
-
* Direct Claude mode reads CLI-managed config files.
|
|
5
|
-
* Multimodel mode uses the structured `mcp list --json` runtime contract.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
|
|
9
|
-
|
|
10
|
-
import type { ExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
|
|
11
|
-
import type { InstalledMcpEntry } from '@shared/types/extensions';
|
|
12
|
-
|
|
13
|
-
const CACHE_TTL_MS = 10_000; // 10 seconds
|
|
14
|
-
|
|
15
|
-
interface TimedCache<T> {
|
|
16
|
-
data: T;
|
|
17
|
-
fetchedAt: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class McpInstallationStateService {
|
|
21
|
-
private cache = new Map<string, TimedCache<InstalledMcpEntry[]>>();
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
private readonly runtimeAdapter: ExtensionsRuntimeAdapter = createExtensionsRuntimeAdapter()
|
|
25
|
-
) {}
|
|
26
|
-
|
|
27
|
-
async getInstalled(projectPath?: string): Promise<InstalledMcpEntry[]> {
|
|
28
|
-
const cacheKey = `${this.runtimeAdapter.flavor}:${projectPath ?? '__user__'}`;
|
|
29
|
-
const cached = this.cache.get(cacheKey);
|
|
30
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
31
|
-
return cached.data;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const entries = await this.runtimeAdapter.getInstalledMcp(projectPath);
|
|
35
|
-
this.cache.set(cacheKey, { data: entries, fetchedAt: Date.now() });
|
|
36
|
-
return entries;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
invalidateCache(): void {
|
|
40
|
-
this.cache.clear();
|
|
41
|
-
}
|
|
42
|
-
}
|