@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.
Files changed (91) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-B_QAoeaA.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-PB9luAZU.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-Dfzcp_Ry.js → _basePickBy-CIhniz70.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-B5u2Yiq2.js → _baseUniq-cKAW4Q8I.js} +1 -1
  5. package/dist-renderer/assets/{arc-DElOI7qz.js → arc-YmNsoDXW.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Cf6f4tCu.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-FJUdo9Ry.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-BvJQS9lb.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
  9. package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-n-SGLbin.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-Dwle9tlA.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-Dic8YxQz.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-3n5jC1jk.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-BqizUB3O.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-JRDmD8o9.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-BxFpQw92.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-ByqPwtW9.js → chunk-TZMSLE5B-B0xGXInl.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +1 -0
  20. package/dist-renderer/assets/clone-BTNuUva-.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CVztr86T.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-CIui920O.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-CyL8-bgb.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-CM_67YoY.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DtrtPSGg.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-bOICzF9d.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CJV1g9Hr.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CXbhDo09.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vbXopTpS.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
  30. package/dist-renderer/assets/{graph-CY2T-j4q.js → graph-DOe5j8dH.js} +1 -1
  31. package/dist-renderer/assets/{index-Dv7q-OB0.js → index-B2Dy7M2G.js} +1 -1
  32. package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
  33. package/dist-renderer/assets/{index-CpyChjme.js → index-BySQS7AB.js} +1 -1
  34. package/dist-renderer/assets/{index-S-i9egm8.js → index-C_okzZXP.js} +1 -1
  35. package/dist-renderer/assets/{index-Mrh4pTHw.js → index-CzWxVCRL.js} +1 -1
  36. package/dist-renderer/assets/{index-Dn-BpzSm.js → index-V7dAKPqd.js} +571 -607
  37. package/dist-renderer/assets/{index-C9ONRXVI.js → index-VJ-MM9xa.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-BNg14AdU.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-2_PkPCiu.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DjTx7qoU.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
  41. package/dist-renderer/assets/{layout-DZlHGGN0.js → layout-B-z3y17c.js} +1 -1
  42. package/dist-renderer/assets/{linear-DnlOm48z.js → linear-D-RTX5UW.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-B-nrgt7V.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BToJFvWR.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C0qoxXvH.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DCKGwsGN.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CS6JCcu7.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-C9pAWoSR.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BTTX_v1m.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BHk97lQJ.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CSWCEzCQ.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CmiIc68g.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-DhwSTphI.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +2 -2
  56. package/src/main/ipc/extensions.ts +29 -50
  57. package/src/main/server.ts +17 -26
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -51
  59. package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
  61. package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -7
  62. package/src/renderer/api/httpClient.ts +108 -22
  63. package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -96
  64. package/src/renderer/components/extensions/plugins/PluginCard.tsx +8 -0
  65. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +13 -8
  66. package/src/renderer/components/team/TeamDetailView.tsx +15 -0
  67. package/src/renderer/components/team/tools/AddMcpInline.tsx +47 -0
  68. package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
  69. package/src/renderer/components/team/tools/McpChip.tsx +42 -0
  70. package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
  71. package/src/renderer/components/team/tools/ToolsSection.tsx +208 -0
  72. package/src/shared/types/extensions/api.ts +9 -0
  73. package/src/shared/types/extensions/index.ts +4 -0
  74. package/src/shared/types/extensions/mcp.ts +41 -0
  75. package/src/shared/utils/extensionNormalizers.ts +22 -0
  76. package/dist-renderer/assets/channel-DnbgZg0A.js +0 -1
  77. package/dist-renderer/assets/classDiagram-2ON5EDUG-BAD4p014.js +0 -1
  78. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BAD4p014.js +0 -1
  79. package/dist-renderer/assets/clone-CRX5ZTPd.js +0 -1
  80. package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
  81. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
  82. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
  83. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
  84. package/src/main/services/extensions/install/McpInstallService.ts +0 -407
  85. package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
  86. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
  87. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
  88. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
  89. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
  90. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
  91. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
@@ -1,765 +0,0 @@
1
- /**
2
- * McpServerDetailDialog — full detail view for a single MCP server with install controls.
3
- * Uses Radix UI Kit for all form elements.
4
- */
5
-
6
- import { useEffect, useRef, useState } from 'react';
7
-
8
- import { api } from '@renderer/api';
9
- import { Badge } from '@renderer/components/ui/badge';
10
- import { Button } from '@renderer/components/ui/button';
11
- import {
12
- Dialog,
13
- DialogContent,
14
- DialogDescription,
15
- DialogHeader,
16
- DialogTitle,
17
- } from '@renderer/components/ui/dialog';
18
- import {
19
- Tooltip,
20
- TooltipContent,
21
- TooltipProvider,
22
- TooltipTrigger,
23
- } from '@renderer/components/ui/tooltip';
24
- import { Input } from '@renderer/components/ui/input';
25
- import { Label } from '@renderer/components/ui/label';
26
- import {
27
- Select,
28
- SelectContent,
29
- SelectItem,
30
- SelectTrigger,
31
- SelectValue,
32
- } from '@renderer/components/ui/select';
33
- import { useStore } from '@renderer/store';
34
- import {
35
- getExtensionActionDisableReason,
36
- getMcpInstallationSummaryLabel,
37
- getMcpOperationKey,
38
- getPreferredMcpInstallationEntry,
39
- sanitizeMcpServerName,
40
- } from '@shared/utils/extensionNormalizers';
41
- import {
42
- getDefaultMcpSharedScope,
43
- getMcpScopeLabel,
44
- isProjectScopedMcpScope,
45
- isSharedMcpScope,
46
- } from '@shared/utils/mcpScopes';
47
- import { Check, ExternalLink, Loader2, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
48
-
49
- import { InstallButton } from '../common/InstallButton';
50
- import { HarnessSelector } from '../common/HarnessSelector';
51
- import { SourceBadge } from '../common/SourceBadge';
52
-
53
- import type { CliInstallationStatus } from '@shared/types';
54
- import type {
55
- InstalledMcpEntry,
56
- McpCatalogItem,
57
- McpHeaderDef,
58
- McpServerDiagnostic,
59
- } from '@shared/types/extensions';
60
-
61
- interface McpServerDetailDialogProps {
62
- server: McpCatalogItem | null;
63
- isInstalled: boolean;
64
- installedEntry?: InstalledMcpEntry | null;
65
- installedEntries?: InstalledMcpEntry[];
66
- diagnostic?: McpServerDiagnostic | null;
67
- diagnosticsLoading?: boolean;
68
- projectPath: string | null;
69
- open: boolean;
70
- onClose: () => void;
71
- cliStatus?: Pick<
72
- CliInstallationStatus,
73
- 'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError' | 'flavor' | 'providers'
74
- > | null;
75
- cliStatusLoading?: boolean;
76
- }
77
-
78
- type Scope = 'local' | 'user' | 'project' | 'global';
79
-
80
- export const McpServerDetailDialog = ({
81
- server,
82
- isInstalled,
83
- installedEntry,
84
- installedEntries = [],
85
- diagnostic,
86
- diagnosticsLoading,
87
- projectPath,
88
- open,
89
- onClose,
90
- cliStatus: cliStatusOverride,
91
- cliStatusLoading,
92
- }: McpServerDetailDialogProps): React.JSX.Element => {
93
- const storedCliStatus = useStore((s) => s.cliStatus);
94
- const cliStatus = cliStatusOverride ?? storedCliStatus;
95
- const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
96
- const [scope, setScope] = useState<Scope>(defaultSharedScope);
97
- const operationKey = server ? getMcpOperationKey(server.id, scope, projectPath) : null;
98
- const installProgress = useStore(
99
- (s) => (operationKey ? s.mcpInstallProgress[operationKey] : undefined) ?? 'idle'
100
- );
101
- const installMcpServer = useStore((s) => s.installMcpServer);
102
- const uninstallMcpServer = useStore((s) => s.uninstallMcpServer);
103
- const installError = useStore((s) => (operationKey ? s.installErrors[operationKey] : undefined));
104
- const stars = useStore((s) =>
105
- server?.repositoryUrl ? s.mcpGitHubStars[server.repositoryUrl] : undefined
106
- );
107
-
108
- const [serverName, setServerName] = useState('');
109
- const [harnessType, setHarnessType] = useState('claudecode');
110
- const [envValues, setEnvValues] = useState<Record<string, string>>({});
111
- const [headers, setHeaders] = useState<McpHeaderDef[]>([]);
112
- const [imgError, setImgError] = useState(false);
113
- const [autoFilledFields, setAutoFilledFields] = useState<Set<string>>(new Set());
114
- const autoFilledValuesRef = useRef<Record<string, string>>({});
115
- const previousDefaultSharedScopeRef = useRef<Scope>(defaultSharedScope);
116
- const normalizedInstalledEntries = installedEntries.length
117
- ? installedEntries
118
- : installedEntry
119
- ? [installedEntry]
120
- : [];
121
- const scopeOptions: { value: Scope; label: string }[] = [
122
- { value: defaultSharedScope, label: getMcpScopeLabel(defaultSharedScope, cliStatus?.flavor) },
123
- ...(defaultSharedScope !== 'user' &&
124
- normalizedInstalledEntries.some((entry) => entry.scope === 'user')
125
- ? [{ value: 'user' as const, label: getMcpScopeLabel('user', cliStatus?.flavor) }]
126
- : []),
127
- { value: 'project', label: '项目' },
128
- { value: 'local', label: '本地' },
129
- ];
130
- const preferredInstalledEntry = getPreferredMcpInstallationEntry(normalizedInstalledEntries);
131
- const selectedInstalledEntry =
132
- normalizedInstalledEntries.find((entry) => entry.scope === scope) ?? null;
133
- const installSummaryLabel = getMcpInstallationSummaryLabel(normalizedInstalledEntries);
134
- const envVarLookupNames =
135
- server?.envVars
136
- .map((entry) => entry.name)
137
- .sort()
138
- .join('\0') ?? '';
139
- const statusSectionLabel =
140
- cliStatus?.flavor === 'agent_teams_orchestrator' ? '运行时状态' : 'Claude 状态';
141
- const apiKeyLookupProjectPath = isProjectScopedMcpScope(scope)
142
- ? (projectPath ?? undefined)
143
- : undefined;
144
-
145
- // Initialize form when dialog opens or server changes
146
- useEffect(() => {
147
- if (!server || !open) {
148
- return;
149
- }
150
-
151
- setEnvValues(Object.fromEntries(server.envVars.map((env) => [env.name, ''])));
152
- setHeaders(
153
- (server.authHeaders ?? []).map((header) => ({
154
- key: header.key,
155
- value: '',
156
- secret: header.isSecret,
157
- description: header.description,
158
- isRequired: header.isRequired,
159
- valueTemplate: header.valueTemplate,
160
- locked: true,
161
- }))
162
- );
163
- setServerName(preferredInstalledEntry?.name ?? sanitizeMcpServerName(server.name));
164
- setScope((preferredInstalledEntry?.scope as Scope | undefined) ?? defaultSharedScope);
165
- setImgError(false);
166
- setAutoFilledFields(new Set());
167
- autoFilledValuesRef.current = {};
168
- }, [open, preferredInstalledEntry?.name, preferredInstalledEntry?.scope, server?.id]);
169
-
170
- useEffect(() => {
171
- if (!open) {
172
- previousDefaultSharedScopeRef.current = defaultSharedScope;
173
- return;
174
- }
175
-
176
- const previousDefaultSharedScope = previousDefaultSharedScopeRef.current;
177
- if (
178
- previousDefaultSharedScope !== defaultSharedScope &&
179
- !preferredInstalledEntry &&
180
- scope === previousDefaultSharedScope &&
181
- isSharedMcpScope(scope)
182
- ) {
183
- setScope(defaultSharedScope);
184
- }
185
-
186
- previousDefaultSharedScopeRef.current = defaultSharedScope;
187
- }, [defaultSharedScope, open, preferredInstalledEntry, scope]);
188
-
189
- useEffect(() => {
190
- if (!server || !open || !selectedInstalledEntry) {
191
- return;
192
- }
193
-
194
- setServerName(selectedInstalledEntry.name);
195
- }, [open, selectedInstalledEntry, server]);
196
-
197
- useEffect(() => {
198
- if (open && isProjectScopedMcpScope(scope) && !projectPath) {
199
- setScope(defaultSharedScope);
200
- }
201
- }, [defaultSharedScope, open, projectPath, scope]);
202
-
203
- // Auto-fill env values from saved API keys
204
- useEffect(() => {
205
- if (!server || !open || server.envVars.length === 0 || !api.apiKeys) return;
206
-
207
- const envVarNames = server.envVars.map((e) => e.name);
208
- void api.apiKeys.lookup(envVarNames, apiKeyLookupProjectPath).then(
209
- (results) => {
210
- const previousAutoFilledValues = autoFilledValuesRef.current;
211
- const nextAutoFilledValues: Record<string, string> = {};
212
- for (const r of results) {
213
- nextAutoFilledValues[r.envVarName] = r.value;
214
- }
215
- setEnvValues((prev) => {
216
- const next = { ...prev };
217
-
218
- for (const [envVarName, previousValue] of Object.entries(previousAutoFilledValues)) {
219
- if (!(envVarName in nextAutoFilledValues) && next[envVarName] === previousValue) {
220
- next[envVarName] = '';
221
- }
222
- }
223
-
224
- for (const [envVarName, nextValue] of Object.entries(nextAutoFilledValues)) {
225
- if (!next[envVarName] || next[envVarName] === previousAutoFilledValues[envVarName]) {
226
- next[envVarName] = nextValue;
227
- }
228
- }
229
-
230
- return next;
231
- });
232
- setAutoFilledFields(new Set(Object.keys(nextAutoFilledValues)));
233
- autoFilledValuesRef.current = nextAutoFilledValues;
234
- },
235
- () => {
236
- // Silently fail — auto-fill is supplementary
237
- }
238
- );
239
- }, [apiKeyLookupProjectPath, envVarLookupNames, open, server?.id]); // eslint-disable-line react-hooks/exhaustive-deps
240
-
241
- if (!server) return <></>;
242
-
243
- const canAutoInstall = !!server.installSpec;
244
- const isHttp = server.installSpec?.type === 'http';
245
- const hasIcon = !!server.iconUrl && !imgError;
246
- const npmPackageUrl =
247
- server.installSpec?.type === 'stdio'
248
- ? `https://www.npmjs.com/package/${server.installSpec.npmPackage}`
249
- : null;
250
- const hasSuggestedHeaders = headers.some((header) => header.locked);
251
- const missingRequiredEnvVars = server.envVars.some(
252
- (env) => env.isRequired && !envValues[env.name]?.trim()
253
- );
254
- const missingRequiredHeaders = headers.some(
255
- (header) => header.isRequired && !header.value.trim()
256
- );
257
- const isInstalledForScope = selectedInstalledEntry !== null;
258
- const uninstallServerName = selectedInstalledEntry?.name ?? serverName;
259
- const uninstallScope = selectedInstalledEntry?.scope ?? scope;
260
- const scopeRequiresProjectPath = isProjectScopedMcpScope(scope) && !projectPath;
261
- const installDisabled =
262
- !serverName.trim() ||
263
- missingRequiredEnvVars ||
264
- missingRequiredHeaders ||
265
- scopeRequiresProjectPath;
266
- const installCliDisableReason = getExtensionActionDisableReason({
267
- isInstalled: false,
268
- cliStatus,
269
- cliStatusLoading: cliStatusLoading ?? false,
270
- section: 'mcp',
271
- });
272
- const uninstallCliDisableReason = getExtensionActionDisableReason({
273
- isInstalled: true,
274
- cliStatus,
275
- cliStatusLoading: cliStatusLoading ?? false,
276
- section: 'mcp',
277
- });
278
- const diagnosticBadgeClass =
279
- diagnostic?.status === 'connected'
280
- ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400'
281
- : diagnostic?.status === 'needs-authentication'
282
- ? 'border-amber-500/30 bg-amber-500/10 text-amber-400'
283
- : diagnostic?.status === 'failed'
284
- ? 'border-red-500/30 bg-red-500/10 text-red-400'
285
- : 'border-border bg-surface-raised text-text-muted';
286
-
287
- const handleInstall = () => {
288
- installMcpServer({
289
- registryId: server.id,
290
- serverName,
291
- scope,
292
- projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
293
- envValues,
294
- headers,
295
- harnessType,
296
- });
297
- };
298
-
299
- const handleUninstall = () => {
300
- uninstallMcpServer(
301
- server.id,
302
- uninstallServerName,
303
- uninstallScope,
304
- isProjectScopedMcpScope(uninstallScope) ? (projectPath ?? undefined) : undefined
305
- );
306
- };
307
-
308
- const addHeader = () => {
309
- setHeaders((prev) => [...prev, { key: '', value: '' }]);
310
- };
311
-
312
- const removeHeader = (index: number) => {
313
- setHeaders((prev) => prev.filter((_, i) => i !== index));
314
- };
315
-
316
- const updateHeader = (index: number, field: 'key' | 'value', value: string) => {
317
- setHeaders((prev) => prev.map((h, i) => (i === index ? { ...h, [field]: value } : h)));
318
- };
319
-
320
- return (
321
- <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
322
- <DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
323
- <DialogHeader>
324
- <div className="flex items-start gap-3">
325
- {/* Server icon (only when available) */}
326
- {hasIcon && (
327
- <div className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-raised">
328
- <img
329
- src={server.iconUrl}
330
- alt=""
331
- className="size-8 rounded object-contain"
332
- onError={() => setImgError(true)}
333
- />
334
- </div>
335
- )}
336
- <div className="min-w-0 flex-1">
337
- <div className="flex items-start justify-between gap-3">
338
- <div className="min-w-0">
339
- <DialogTitle className="truncate">{server.name}</DialogTitle>
340
- <DialogDescription className="mt-1">{server.description}</DialogDescription>
341
- </div>
342
- <div className="flex shrink-0 items-center gap-1.5">
343
- {isInstalled && (
344
- <Badge
345
- className="border-emerald-500/30 bg-emerald-500/10 text-emerald-400"
346
- variant="outline"
347
- >
348
- {installSummaryLabel ?? '已安装'}
349
- </Badge>
350
- )}
351
- {server.source !== 'official' && <SourceBadge source={server.source} />}
352
- </div>
353
- </div>
354
- </div>
355
- </div>
356
- </DialogHeader>
357
-
358
- {/* Metadata grid */}
359
- <div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
360
- <div>
361
- <span className="text-text-muted">来源</span>
362
- <p className="capitalize text-text">{server.source}</p>
363
- </div>
364
- {stars != null && (
365
- <div>
366
- <span className="text-text-muted">GitHub 星标</span>
367
- <p className="flex items-center gap-1 text-text">
368
- <Star className="size-3.5 fill-amber-400 text-amber-400" />
369
- {stars.toLocaleString()}
370
- </p>
371
- </div>
372
- )}
373
- {server.version && (
374
- <div>
375
- <span className="text-text-muted">版本</span>
376
- <p className="text-text">{server.version}</p>
377
- </div>
378
- )}
379
- {server.license && (
380
- <div>
381
- <span className="text-text-muted">许可证</span>
382
- <p className="text-text">{server.license}</p>
383
- </div>
384
- )}
385
- <div>
386
- <span className="text-text-muted">安装类型</span>
387
- {server.installSpec?.type === 'stdio' ? (
388
- <Button
389
- variant="link"
390
- className="h-auto p-0 text-sm text-blue-400"
391
- onClick={() => void api.openExternal(npmPackageUrl!)}
392
- >
393
- npm: {server.installSpec.npmPackage}
394
- </Button>
395
- ) : (
396
- <p className="text-text">
397
- {server.installSpec ? `HTTP: ${server.installSpec.transportType}` : '需要手动设置'}
398
- </p>
399
- )}
400
- </div>
401
- {server.author && (
402
- <div>
403
- <span className="text-text-muted">作者</span>
404
- <p className="text-text">{server.author}</p>
405
- </div>
406
- )}
407
- {server.hostingType && (
408
- <div>
409
- <span className="text-text-muted">托管方式</span>
410
- <p className="capitalize text-text">{server.hostingType}</p>
411
- </div>
412
- )}
413
- {server.publishedAt && (
414
- <div>
415
- <span className="text-text-muted">发布时间</span>
416
- <p className="text-text">{new Date(server.publishedAt).toLocaleDateString()}</p>
417
- </div>
418
- )}
419
- {server.updatedAt && (
420
- <div>
421
- <span className="text-text-muted">更新时间</span>
422
- <p className="text-text">{new Date(server.updatedAt).toLocaleDateString()}</p>
423
- </div>
424
- )}
425
- </div>
426
-
427
- {/* Auth indicator */}
428
- {server.requiresAuth && (
429
- <div className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-sm text-amber-400">
430
- <Lock className="size-4" />
431
- 此服务器需要认证
432
- </div>
433
- )}
434
- {isHttp && !server.requiresAuth && (server.authHeaders?.length ?? 0) === 0 && (
435
- <div className="rounded-md border border-blue-500/30 bg-blue-500/5 px-3 py-2 text-sm text-blue-400">
436
- 远程 MCP 服务器即使未在注册表中声明,也可能仍需要自定义请求头或 API
437
- 密钥。若安装后连接失败,请查看提供商文档。
438
- </div>
439
- )}
440
- {isInstalledForScope && (
441
- <div className="space-y-2 rounded-md border border-border bg-surface-raised px-4 py-3">
442
- <div className="flex items-center justify-between gap-3">
443
- <span className="text-sm font-medium text-text">{statusSectionLabel}</span>
444
- {diagnosticsLoading && !diagnostic ? (
445
- <Badge
446
- className="border-border bg-surface-raised text-text-muted"
447
- variant="outline"
448
- >
449
- 检查中...
450
- </Badge>
451
- ) : diagnostic ? (
452
- <Badge className={diagnosticBadgeClass} variant="outline">
453
- {diagnostic.statusLabel}
454
- </Badge>
455
- ) : (
456
- <Badge
457
- className="border-border bg-surface-raised text-text-muted"
458
- variant="outline"
459
- >
460
- 未检查
461
- </Badge>
462
- )}
463
- </div>
464
- {diagnostic?.target && (
465
- <div>
466
- <p className="mb-1 text-xs text-text-muted">启动目标</p>
467
- <code className="block overflow-x-auto rounded bg-surface px-2 py-1 text-xs text-text">
468
- {diagnostic.target}
469
- </code>
470
- </div>
471
- )}
472
- </div>
473
- )}
474
-
475
- {/* Install form */}
476
- {canAutoInstall && (
477
- <div className="space-y-3 rounded-md border border-border bg-surface-raised p-4">
478
- <h4 className="text-sm font-medium text-text">
479
- {isInstalledForScope ? '管理安装' : '安装服务器'}
480
- </h4>
481
-
482
- {/* Server name */}
483
- <div className="space-y-1.5">
484
- <Label htmlFor="server-name" className="text-xs">
485
- 服务器名称
486
- </Label>
487
- <Input
488
- id="server-name"
489
- value={serverName}
490
- onChange={(e) => setServerName(e.target.value)}
491
- placeholder="my-server"
492
- className="h-8 text-sm"
493
- disabled={isInstalledForScope}
494
- />
495
- </div>
496
-
497
- {/* Scope */}
498
- <div className="space-y-1.5">
499
- <Label className="text-xs">范围</Label>
500
- <Select value={scope} onValueChange={(v) => setScope(v as Scope)}>
501
- <SelectTrigger className="h-8 text-sm">
502
- <SelectValue />
503
- </SelectTrigger>
504
- <SelectContent>
505
- {scopeOptions.map((opt) => (
506
- <SelectItem
507
- key={opt.value}
508
- value={opt.value}
509
- disabled={isProjectScopedMcpScope(opt.value) && !projectPath}
510
- >
511
- {opt.label}
512
- </SelectItem>
513
- ))}
514
- </SelectContent>
515
- </Select>
516
- </div>
517
-
518
- {/* Harness selector */}
519
- <HarnessSelector
520
- capability="mcp"
521
- value={harnessType}
522
- onChange={setHarnessType}
523
- disabled={isInstalledForScope}
524
- />
525
-
526
- {/* Environment variables */}
527
- {server.envVars.length > 0 && (
528
- <div className="space-y-1.5">
529
- <Label className="text-xs">环境变量</Label>
530
- <div className="space-y-2">
531
- {server.envVars.map((env) => (
532
- <div key={env.name} className="flex items-center gap-2">
533
- <code className="w-40 shrink-0 truncate text-xs text-blue-400">
534
- {env.name}
535
- </code>
536
- <Input
537
- type={env.isSecret ? 'password' : 'text'}
538
- value={envValues[env.name] ?? ''}
539
- onChange={(e) =>
540
- setEnvValues((prev) => ({ ...prev, [env.name]: e.target.value }))
541
- }
542
- className="h-7 flex-1 text-xs"
543
- placeholder={env.description ?? env.name}
544
- />
545
- {autoFilledFields.has(env.name) && envValues[env.name] && (
546
- <span className="shrink-0 text-[10px] text-emerald-400">已自动填充</span>
547
- )}
548
- </div>
549
- ))}
550
- </div>
551
- </div>
552
- )}
553
-
554
- {/* Headers (for HTTP/SSE servers) */}
555
- {isHttp && (
556
- <div className="space-y-1.5">
557
- <div className="flex items-center justify-between">
558
- <Label className="text-xs">请求头</Label>
559
- <Button
560
- variant="ghost"
561
- size="sm"
562
- onClick={addHeader}
563
- className="h-6 px-1.5 text-xs"
564
- >
565
- <Plus className="mr-1 size-3" />
566
- {hasSuggestedHeaders ? '添加自定义' : '添加'}
567
- </Button>
568
- </div>
569
- {headers.length > 0 && (
570
- <div className="space-y-2">
571
- {headers.map((header, index) => (
572
- <div key={index} className="space-y-1">
573
- <div className="flex items-center gap-2">
574
- {header.locked ? (
575
- <code className="w-32 shrink-0 truncate text-xs text-blue-400">
576
- {header.key}
577
- </code>
578
- ) : (
579
- <Input
580
- value={header.key}
581
- onChange={(e) => updateHeader(index, 'key', e.target.value)}
582
- className="h-7 w-32 text-xs"
583
- placeholder="Header-Name"
584
- />
585
- )}
586
- <Input
587
- type={header.secret ? 'password' : 'text'}
588
- value={header.value}
589
- onChange={(e) => updateHeader(index, 'value', e.target.value)}
590
- className="h-7 flex-1 text-xs"
591
- placeholder={header.valueTemplate ?? header.description ?? 'value'}
592
- />
593
- <Button
594
- variant="ghost"
595
- size="icon"
596
- className="size-7 text-red-400 hover:bg-red-500/10"
597
- onClick={() => removeHeader(index)}
598
- disabled={header.locked && header.isRequired}
599
- >
600
- <Trash2 className="size-3" />
601
- </Button>
602
- </div>
603
- {(header.description || header.valueTemplate || header.isRequired) && (
604
- <p className="text-[10px] text-text-muted">
605
- {[
606
- header.isRequired ? '必填' : null,
607
- header.description,
608
- header.valueTemplate,
609
- ]
610
- .filter(Boolean)
611
- .join(' • ')}
612
- </p>
613
- )}
614
- </div>
615
- ))}
616
- </div>
617
- )}
618
- </div>
619
- )}
620
-
621
- {/* Install / Save & Restart / Uninstall */}
622
- <div className="flex items-center justify-end gap-2 pt-1">
623
- {isInstalledForScope ? (
624
- <>
625
- <Button
626
- variant="ghost"
627
- size="sm"
628
- className="border-red-500/30 text-red-400 hover:bg-red-500/10"
629
- data-testid="uninstall-button"
630
- onClick={handleUninstall}
631
- disabled={Boolean(uninstallCliDisableReason)}
632
- >
633
- <Trash2 className="size-3.5" />
634
- <span className="ml-1.5">卸载</span>
635
- </Button>
636
- {installProgress === 'pending' ? (
637
- <Button size="default" disabled>
638
- <Loader2 className="size-3.5 animate-spin" />
639
- <span className="ml-1.5">保存并重启中...</span>
640
- </Button>
641
- ) : installProgress === 'success' ? (
642
- <Button size="default" disabled className="text-green-400">
643
- <Check className="size-3.5" />
644
- <span className="ml-1.5">完成</span>
645
- </Button>
646
- ) : installProgress === 'error' ? (
647
- <div className="flex max-w-64 flex-col items-end gap-1">
648
- <TooltipProvider>
649
- <Tooltip>
650
- <TooltipTrigger asChild>
651
- <span tabIndex={0}>
652
- <Button
653
- size="default"
654
- variant="outline"
655
- className="border-red-500/30 text-red-400 hover:bg-red-500/10"
656
- onClick={handleInstall}
657
- disabled={installDisabled || Boolean(installCliDisableReason)}
658
- >
659
- 保存并重启
660
- </Button>
661
- </span>
662
- </TooltipTrigger>
663
- {installError && (
664
- <TooltipContent className="max-w-64 text-red-300">
665
- {installError}
666
- </TooltipContent>
667
- )}
668
- </Tooltip>
669
- </TooltipProvider>
670
- {installError && (
671
- <p className="text-right text-[11px] leading-4 text-red-300">
672
- {installError}
673
- </p>
674
- )}
675
- </div>
676
- ) : (
677
- <Button
678
- size="default"
679
- data-testid="save-restart-button"
680
- onClick={handleInstall}
681
- disabled={installDisabled || Boolean(installCliDisableReason)}
682
- >
683
- 保存并重启
684
- </Button>
685
- )}
686
- </>
687
- ) : (
688
- <InstallButton
689
- state={installProgress}
690
- isInstalled={false}
691
- section="mcp"
692
- cliStatus={cliStatus}
693
- cliStatusLoading={cliStatusLoading}
694
- onInstall={handleInstall}
695
- onUninstall={handleUninstall}
696
- disabled={installDisabled}
697
- size="default"
698
- errorMessage={installError}
699
- />
700
- )}
701
- </div>
702
- </div>
703
- )}
704
-
705
- {!canAutoInstall && (
706
- <div className="rounded-md border border-border bg-surface-raised px-4 py-3 text-sm text-text-muted">
707
- 此服务器需要手动设置。请查看仓库中的安装说明。
708
- </div>
709
- )}
710
-
711
- {/* Tools */}
712
- {server.tools.length > 0 && (
713
- <div>
714
- <h4 className="mb-2 flex items-center gap-1.5 text-sm font-medium text-text">
715
- <Wrench className="size-4" />
716
- 工具({server.tools.length})
717
- </h4>
718
- <div className="max-h-48 space-y-1 overflow-y-auto">
719
- {server.tools.map((tool) => (
720
- <div key={tool.name} className="rounded-md bg-surface-raised p-2 text-xs">
721
- <code className="font-mono text-text">{tool.name}</code>
722
- {tool.description && <p className="mt-0.5 text-text-muted">{tool.description}</p>}
723
- </div>
724
- ))}
725
- </div>
726
- </div>
727
- )}
728
-
729
- {/* Links */}
730
- <div className="flex items-center gap-4">
731
- {server.repositoryUrl && (
732
- <Button
733
- variant="link"
734
- className="h-auto p-0 text-sm text-blue-400"
735
- onClick={() => void api.openExternal(server.repositoryUrl!)}
736
- >
737
- <ExternalLink className="mr-1 size-3.5" />
738
- 仓库
739
- </Button>
740
- )}
741
- {server.glamaUrl && (
742
- <Button
743
- variant="link"
744
- className="h-auto p-0 text-sm text-blue-400"
745
- onClick={() => void api.openExternal(server.glamaUrl!)}
746
- >
747
- <ExternalLink className="mr-1 size-3.5" />
748
- Glama
749
- </Button>
750
- )}
751
- {server.websiteUrl && (
752
- <Button
753
- variant="link"
754
- className="h-auto p-0 text-sm text-blue-400"
755
- onClick={() => void api.openExternal(server.websiteUrl!)}
756
- >
757
- <ExternalLink className="mr-1 size-3.5" />
758
- 网站
759
- </Button>
760
- )}
761
- </div>
762
- </DialogContent>
763
- </Dialog>
764
- );
765
- };