@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,593 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* McpServersPanel — search and browse the MCP server catalog.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
-
|
|
7
|
-
import { Badge } from '@renderer/components/ui/badge';
|
|
8
|
-
import { Button } from '@renderer/components/ui/button';
|
|
9
|
-
import {
|
|
10
|
-
Select,
|
|
11
|
-
SelectContent,
|
|
12
|
-
SelectItem,
|
|
13
|
-
SelectTrigger,
|
|
14
|
-
SelectValue,
|
|
15
|
-
} from '@renderer/components/ui/select';
|
|
16
|
-
import { useStore } from '@renderer/store';
|
|
17
|
-
import { formatRelativeTime } from '@renderer/utils/formatters';
|
|
18
|
-
import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
|
|
19
|
-
import { CLI_NOT_FOUND_MARKER } from '@shared/constants/cli';
|
|
20
|
-
import {
|
|
21
|
-
getMcpDiagnosticKey,
|
|
22
|
-
getMcpOperationKey,
|
|
23
|
-
getMcpProjectStateKey,
|
|
24
|
-
getPreferredMcpInstallationEntry,
|
|
25
|
-
sanitizeMcpServerName,
|
|
26
|
-
} from '@shared/utils/extensionNormalizers';
|
|
27
|
-
import { AlertTriangle, RefreshCw, Search, Server, Trash2 } from 'lucide-react';
|
|
28
|
-
import { useShallow } from 'zustand/react/shallow';
|
|
29
|
-
|
|
30
|
-
import { SearchInput } from '../common/SearchInput';
|
|
31
|
-
|
|
32
|
-
import { McpServerCard } from './McpServerCard';
|
|
33
|
-
import { McpServerDetailDialog } from './McpServerDetailDialog';
|
|
34
|
-
|
|
35
|
-
import type { CliInstallationStatus } from '@shared/types';
|
|
36
|
-
import type {
|
|
37
|
-
InstalledMcpEntry,
|
|
38
|
-
InstallScope,
|
|
39
|
-
McpCatalogItem,
|
|
40
|
-
McpServerDiagnostic,
|
|
41
|
-
} from '@shared/types/extensions';
|
|
42
|
-
|
|
43
|
-
type McpSortValue = 'name-asc' | 'name-desc' | 'tools-desc';
|
|
44
|
-
|
|
45
|
-
const MCP_SORT_OPTIONS: { value: McpSortValue; label: string }[] = [
|
|
46
|
-
{ value: 'name-asc', label: 'Name A→Z' },
|
|
47
|
-
{ value: 'name-desc', label: 'Name Z→A' },
|
|
48
|
-
{ value: 'tools-desc', label: 'Most tools' },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
function sortMcpServers(servers: McpCatalogItem[], sort: McpSortValue): McpCatalogItem[] {
|
|
52
|
-
return [...servers].sort((a, b) => {
|
|
53
|
-
switch (sort) {
|
|
54
|
-
case 'name-asc':
|
|
55
|
-
return a.name.localeCompare(b.name);
|
|
56
|
-
case 'name-desc':
|
|
57
|
-
return b.name.localeCompare(a.name);
|
|
58
|
-
case 'tools-desc':
|
|
59
|
-
return b.tools.length - a.tools.length;
|
|
60
|
-
default:
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface McpServersPanelProps {
|
|
67
|
-
projectPath: string | null;
|
|
68
|
-
mcpSearchQuery: string;
|
|
69
|
-
mcpSearch: (query: string) => void;
|
|
70
|
-
mcpSearchResults: McpCatalogItem[];
|
|
71
|
-
mcpSearchLoading: boolean;
|
|
72
|
-
mcpSearchWarnings: string[];
|
|
73
|
-
selectedMcpServerId: string | null;
|
|
74
|
-
setSelectedMcpServerId: (id: string | null) => void;
|
|
75
|
-
cliStatus?: Pick<
|
|
76
|
-
CliInstallationStatus,
|
|
77
|
-
| 'installed'
|
|
78
|
-
| 'authLoggedIn'
|
|
79
|
-
| 'binaryPath'
|
|
80
|
-
| 'launchError'
|
|
81
|
-
| 'flavor'
|
|
82
|
-
| 'displayName'
|
|
83
|
-
| 'providers'
|
|
84
|
-
> | null;
|
|
85
|
-
cliStatusLoading?: boolean;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export const McpServersPanel = ({
|
|
89
|
-
projectPath,
|
|
90
|
-
mcpSearchQuery,
|
|
91
|
-
mcpSearch,
|
|
92
|
-
mcpSearchResults,
|
|
93
|
-
mcpSearchLoading,
|
|
94
|
-
mcpSearchWarnings,
|
|
95
|
-
selectedMcpServerId,
|
|
96
|
-
setSelectedMcpServerId,
|
|
97
|
-
cliStatus: cliStatusOverride,
|
|
98
|
-
cliStatusLoading: cliStatusLoadingOverride,
|
|
99
|
-
}: McpServersPanelProps): React.JSX.Element => {
|
|
100
|
-
const projectStateKey = getMcpProjectStateKey(projectPath);
|
|
101
|
-
const {
|
|
102
|
-
browseCatalog,
|
|
103
|
-
browseNextCursor,
|
|
104
|
-
browseLoading,
|
|
105
|
-
browseError,
|
|
106
|
-
mcpBrowse,
|
|
107
|
-
installedServersByProjectPath,
|
|
108
|
-
installedServersFallback,
|
|
109
|
-
fetchMcpGitHubStars,
|
|
110
|
-
mcpDiagnosticsByProjectPath,
|
|
111
|
-
mcpDiagnosticsFallback,
|
|
112
|
-
mcpDiagnosticsLoadingByProjectPath,
|
|
113
|
-
mcpDiagnosticsLoadingFallback,
|
|
114
|
-
mcpDiagnosticsErrorByProjectPath,
|
|
115
|
-
mcpDiagnosticsErrorFallback,
|
|
116
|
-
mcpDiagnosticsLastCheckedAtByProjectPath,
|
|
117
|
-
mcpDiagnosticsLastCheckedAtFallback,
|
|
118
|
-
runMcpDiagnostics,
|
|
119
|
-
uninstallMcpServer,
|
|
120
|
-
mcpInstallProgress,
|
|
121
|
-
} = useStore(
|
|
122
|
-
useShallow((s) => ({
|
|
123
|
-
browseCatalog: s.mcpBrowseCatalog,
|
|
124
|
-
browseNextCursor: s.mcpBrowseNextCursor,
|
|
125
|
-
browseLoading: s.mcpBrowseLoading,
|
|
126
|
-
browseError: s.mcpBrowseError,
|
|
127
|
-
mcpBrowse: s.mcpBrowse,
|
|
128
|
-
installedServersByProjectPath: s.mcpInstalledServersByProjectPath,
|
|
129
|
-
installedServersFallback: s.mcpInstalledServers,
|
|
130
|
-
fetchMcpGitHubStars: s.fetchMcpGitHubStars,
|
|
131
|
-
mcpDiagnosticsByProjectPath: s.mcpDiagnosticsByProjectPath,
|
|
132
|
-
mcpDiagnosticsFallback: s.mcpDiagnostics,
|
|
133
|
-
mcpDiagnosticsLoadingByProjectPath: s.mcpDiagnosticsLoadingByProjectPath,
|
|
134
|
-
mcpDiagnosticsLoadingFallback: s.mcpDiagnosticsLoading,
|
|
135
|
-
mcpDiagnosticsErrorByProjectPath: s.mcpDiagnosticsErrorByProjectPath,
|
|
136
|
-
mcpDiagnosticsErrorFallback: s.mcpDiagnosticsError,
|
|
137
|
-
mcpDiagnosticsLastCheckedAtByProjectPath: s.mcpDiagnosticsLastCheckedAtByProjectPath,
|
|
138
|
-
mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt,
|
|
139
|
-
runMcpDiagnostics: s.runMcpDiagnostics,
|
|
140
|
-
uninstallMcpServer: s.uninstallMcpServer,
|
|
141
|
-
mcpInstallProgress: s.mcpInstallProgress,
|
|
142
|
-
}))
|
|
143
|
-
);
|
|
144
|
-
const storedCliStatus = useStore((s) => s.cliStatus);
|
|
145
|
-
const storedCliStatusLoading = useStore((s) => s.cliStatusLoading);
|
|
146
|
-
const cliStatus = cliStatusOverride ?? storedCliStatus;
|
|
147
|
-
const cliStatusLoading = cliStatusLoadingOverride ?? storedCliStatusLoading;
|
|
148
|
-
const installedServers =
|
|
149
|
-
installedServersByProjectPath?.[projectStateKey] ?? installedServersFallback ?? [];
|
|
150
|
-
const mcpDiagnostics =
|
|
151
|
-
mcpDiagnosticsByProjectPath?.[projectStateKey] ?? mcpDiagnosticsFallback ?? {};
|
|
152
|
-
const mcpDiagnosticsLoading =
|
|
153
|
-
mcpDiagnosticsLoadingByProjectPath?.[projectStateKey] ?? mcpDiagnosticsLoadingFallback ?? false;
|
|
154
|
-
const mcpDiagnosticsError =
|
|
155
|
-
mcpDiagnosticsErrorByProjectPath?.[projectStateKey] ?? mcpDiagnosticsErrorFallback ?? null;
|
|
156
|
-
const mcpDiagnosticsLastCheckedAt =
|
|
157
|
-
mcpDiagnosticsLastCheckedAtByProjectPath?.[projectStateKey] ??
|
|
158
|
-
mcpDiagnosticsLastCheckedAtFallback ??
|
|
159
|
-
null;
|
|
160
|
-
|
|
161
|
-
const [mcpSort, setMcpSort] = useState<McpSortValue>('name-asc');
|
|
162
|
-
|
|
163
|
-
// Load initial browse data
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
if (browseCatalog.length === 0 && !browseLoading && !browseError) {
|
|
166
|
-
void mcpBrowse();
|
|
167
|
-
}
|
|
168
|
-
}, [browseCatalog.length, browseError, browseLoading, mcpBrowse]);
|
|
169
|
-
|
|
170
|
-
const diagnosticsDisableReason = useMemo(() => {
|
|
171
|
-
if (cliStatus === null || typeof cliStatus === 'undefined') {
|
|
172
|
-
return cliStatusLoading ? '正在检查运行时状态...' : '正在检查运行时可用性...';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (cliStatus?.installed === false) {
|
|
176
|
-
if (cliStatus.binaryPath && cliStatus.launchError) {
|
|
177
|
-
return '已找到配置的运行时,但启动失败。请前往首页修复或重新安装。';
|
|
178
|
-
}
|
|
179
|
-
return '需要配置运行时。请前往首页安装或修复。';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return null;
|
|
183
|
-
}, [cliStatus, cliStatusLoading]);
|
|
184
|
-
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
if (diagnosticsDisableReason) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
void runMcpDiagnostics(projectPath ?? undefined);
|
|
190
|
-
}, [diagnosticsDisableReason, projectPath, runMcpDiagnostics]);
|
|
191
|
-
|
|
192
|
-
// Fetch GitHub stars after catalog loads (fire-and-forget)
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
const urls = browseCatalog.map((s) => s.repositoryUrl).filter((u): u is string => !!u);
|
|
195
|
-
if (urls.length > 0) {
|
|
196
|
-
fetchMcpGitHubStars(urls);
|
|
197
|
-
}
|
|
198
|
-
}, [browseCatalog, fetchMcpGitHubStars]);
|
|
199
|
-
|
|
200
|
-
// Decide which list to show: search results or browse
|
|
201
|
-
const isSearching = mcpSearchQuery.trim().length > 0;
|
|
202
|
-
const rawServers = isSearching ? mcpSearchResults : browseCatalog;
|
|
203
|
-
const isLoading = isSearching ? mcpSearchLoading : browseLoading;
|
|
204
|
-
const warnings = isSearching ? mcpSearchWarnings : [];
|
|
205
|
-
|
|
206
|
-
// Installed lookup set (lowercase CLI names)
|
|
207
|
-
const installedNames = useMemo(
|
|
208
|
-
() => new Set(installedServers.map((s) => s.name.toLowerCase())),
|
|
209
|
-
[installedServers]
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
const installedEntriesByName = useMemo(() => {
|
|
213
|
-
const entriesByName = new Map<string, InstalledMcpEntry[]>();
|
|
214
|
-
for (const entry of installedServers) {
|
|
215
|
-
const key = entry.name.toLowerCase();
|
|
216
|
-
entriesByName.set(key, [...(entriesByName.get(key) ?? []), entry]);
|
|
217
|
-
}
|
|
218
|
-
return entriesByName;
|
|
219
|
-
}, [installedServers]);
|
|
220
|
-
|
|
221
|
-
/** Check if a catalog server is installed by comparing sanitized names */
|
|
222
|
-
const isServerInstalled = (server: McpCatalogItem): boolean =>
|
|
223
|
-
installedNames.has(sanitizeMcpServerName(server.name));
|
|
224
|
-
|
|
225
|
-
const getInstalledEntries = (server: McpCatalogItem): InstalledMcpEntry[] =>
|
|
226
|
-
installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? [];
|
|
227
|
-
|
|
228
|
-
const getInstalledEntry = (server: McpCatalogItem): InstalledMcpEntry | null =>
|
|
229
|
-
getPreferredMcpInstallationEntry(getInstalledEntries(server));
|
|
230
|
-
|
|
231
|
-
const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => {
|
|
232
|
-
const installedEntry = getInstalledEntry(server);
|
|
233
|
-
return installedEntry
|
|
234
|
-
? (mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name, installedEntry.scope)] ??
|
|
235
|
-
mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name)] ??
|
|
236
|
-
mcpDiagnostics[installedEntry.name] ??
|
|
237
|
-
null)
|
|
238
|
-
: null;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const allDiagnostics = useMemo(
|
|
242
|
-
() => Object.values(mcpDiagnostics).sort((a, b) => a.name.localeCompare(b.name)),
|
|
243
|
-
[mcpDiagnostics]
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
const getDiagnosticBadgeClass = (status: McpServerDiagnostic['status']): string => {
|
|
247
|
-
switch (status) {
|
|
248
|
-
case 'connected':
|
|
249
|
-
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400';
|
|
250
|
-
case 'needs-authentication':
|
|
251
|
-
return 'border-amber-500/30 bg-amber-500/10 text-amber-400';
|
|
252
|
-
case 'failed':
|
|
253
|
-
return 'border-red-500/30 bg-red-500/10 text-red-400';
|
|
254
|
-
default:
|
|
255
|
-
return 'border-border bg-surface-raised text-text-muted';
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
// Sort displayed catalog servers
|
|
260
|
-
const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]);
|
|
261
|
-
const runtimeLabel = getRuntimeDisplayName(cliStatus, true);
|
|
262
|
-
|
|
263
|
-
// Find selected server (search in both lists to avoid losing selection during search toggle)
|
|
264
|
-
const selectedServer = useMemo(() => {
|
|
265
|
-
if (!selectedMcpServerId) return null;
|
|
266
|
-
return (
|
|
267
|
-
displayServers.find((s) => s.id === selectedMcpServerId) ??
|
|
268
|
-
browseCatalog.find((s) => s.id === selectedMcpServerId) ??
|
|
269
|
-
mcpSearchResults.find((s) => s.id === selectedMcpServerId) ??
|
|
270
|
-
null
|
|
271
|
-
);
|
|
272
|
-
}, [displayServers, browseCatalog, mcpSearchResults, selectedMcpServerId]);
|
|
273
|
-
|
|
274
|
-
return (
|
|
275
|
-
<div className="flex flex-col gap-4">
|
|
276
|
-
<div className="rounded-md border border-black/10 bg-surface-raised px-4 py-3 dark:border-white/10">
|
|
277
|
-
<div className="flex items-center justify-between gap-3">
|
|
278
|
-
<div>
|
|
279
|
-
<p className="text-sm font-medium text-text">MCP 健康状态</p>
|
|
280
|
-
<p className="text-xs text-text-muted">
|
|
281
|
-
{mcpDiagnosticsLoading ? (
|
|
282
|
-
<>正在通过 {runtimeLabel} 检查已安装的 MCP 服务器...</>
|
|
283
|
-
) : diagnosticsDisableReason ? (
|
|
284
|
-
diagnosticsDisableReason
|
|
285
|
-
) : mcpDiagnosticsLastCheckedAt ? (
|
|
286
|
-
`上次检查:${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}`
|
|
287
|
-
) : (
|
|
288
|
-
<>在此页面运行诊断,以验证已安装 MCP 的连接状态。</>
|
|
289
|
-
)}
|
|
290
|
-
</p>
|
|
291
|
-
</div>
|
|
292
|
-
<Button
|
|
293
|
-
variant="outline"
|
|
294
|
-
size="sm"
|
|
295
|
-
onClick={() => void runMcpDiagnostics(projectPath ?? undefined)}
|
|
296
|
-
disabled={mcpDiagnosticsLoading || Boolean(diagnosticsDisableReason)}
|
|
297
|
-
className="whitespace-nowrap"
|
|
298
|
-
>
|
|
299
|
-
<RefreshCw
|
|
300
|
-
className={`mr-1.5 size-3.5 ${mcpDiagnosticsLoading ? 'animate-spin' : ''}`}
|
|
301
|
-
/>
|
|
302
|
-
{mcpDiagnosticsLoading ? '检查中...' : '检查状态'}
|
|
303
|
-
</Button>
|
|
304
|
-
</div>
|
|
305
|
-
|
|
306
|
-
{(mcpDiagnosticsLoading || allDiagnostics.length > 0) && (
|
|
307
|
-
<div className="mt-4 border-t border-black/10 pt-4 dark:border-white/10">
|
|
308
|
-
<div className="mb-3 flex items-center justify-between gap-3">
|
|
309
|
-
<p className="text-sm font-medium text-text">运行时 MCP 诊断</p>
|
|
310
|
-
{allDiagnostics.length > 0 && (
|
|
311
|
-
<span className="text-xs text-text-muted">{allDiagnostics.length} 个服务器</span>
|
|
312
|
-
)}
|
|
313
|
-
</div>
|
|
314
|
-
{allDiagnostics.length > 0 ? (
|
|
315
|
-
<div className="mcp-diagnostics-list max-h-[18.5rem] space-y-2 overflow-y-auto pr-1">
|
|
316
|
-
{allDiagnostics.map((diagnostic) => {
|
|
317
|
-
const opKey = getMcpOperationKey(
|
|
318
|
-
diagnostic.name,
|
|
319
|
-
(diagnostic.scope as InstallScope) || 'user',
|
|
320
|
-
projectPath
|
|
321
|
-
);
|
|
322
|
-
const uninstalling = mcpInstallProgress[opKey] === 'pending';
|
|
323
|
-
return (
|
|
324
|
-
<div
|
|
325
|
-
key={getMcpDiagnosticKey(diagnostic.name, diagnostic.scope)}
|
|
326
|
-
className="flex items-start justify-between gap-3 rounded-md border border-black/10 px-3 py-2 dark:border-white/10"
|
|
327
|
-
>
|
|
328
|
-
<div className="min-w-0 flex-1">
|
|
329
|
-
<div className="flex items-center gap-2">
|
|
330
|
-
<p className="text-sm text-text">{diagnostic.name}</p>
|
|
331
|
-
{diagnostic.scope && (
|
|
332
|
-
<span className="rounded-full border border-border bg-surface-raised px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-text-muted">
|
|
333
|
-
{diagnostic.scope}
|
|
334
|
-
</span>
|
|
335
|
-
)}
|
|
336
|
-
</div>
|
|
337
|
-
<p
|
|
338
|
-
className="truncate font-mono text-[11px] text-text-muted"
|
|
339
|
-
title={diagnostic.target}
|
|
340
|
-
>
|
|
341
|
-
{diagnostic.target}
|
|
342
|
-
</p>
|
|
343
|
-
</div>
|
|
344
|
-
<div className="flex items-center gap-2">
|
|
345
|
-
<Badge
|
|
346
|
-
className={getDiagnosticBadgeClass(diagnostic.status)}
|
|
347
|
-
variant="outline"
|
|
348
|
-
>
|
|
349
|
-
{diagnostic.statusLabel}
|
|
350
|
-
</Badge>
|
|
351
|
-
<Button
|
|
352
|
-
variant="ghost"
|
|
353
|
-
size="sm"
|
|
354
|
-
className="size-6 p-0 text-red-300 hover:text-red-200"
|
|
355
|
-
disabled={uninstalling}
|
|
356
|
-
onClick={() => {
|
|
357
|
-
void uninstallMcpServer(
|
|
358
|
-
diagnostic.name,
|
|
359
|
-
diagnostic.name,
|
|
360
|
-
diagnostic.scope || undefined,
|
|
361
|
-
projectPath ?? undefined
|
|
362
|
-
);
|
|
363
|
-
}}
|
|
364
|
-
title={`卸载 ${diagnostic.name}`}
|
|
365
|
-
>
|
|
366
|
-
<Trash2 className="size-3.5" />
|
|
367
|
-
</Button>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
);
|
|
371
|
-
})}
|
|
372
|
-
</div>
|
|
373
|
-
) : (
|
|
374
|
-
<p className="text-xs text-text-muted">正在等待诊断结果...</p>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
377
|
-
)}
|
|
378
|
-
</div>
|
|
379
|
-
|
|
380
|
-
{/* Search + sort row */}
|
|
381
|
-
<div className="flex items-center gap-3">
|
|
382
|
-
<div className="flex-1">
|
|
383
|
-
<SearchInput
|
|
384
|
-
value={mcpSearchQuery}
|
|
385
|
-
onChange={mcpSearch}
|
|
386
|
-
placeholder="搜索 MCP 服务器..."
|
|
387
|
-
/>
|
|
388
|
-
</div>
|
|
389
|
-
<Select value={mcpSort} onValueChange={(v) => setMcpSort(v as McpSortValue)}>
|
|
390
|
-
<SelectTrigger className="w-36">
|
|
391
|
-
<SelectValue />
|
|
392
|
-
</SelectTrigger>
|
|
393
|
-
<SelectContent>
|
|
394
|
-
{MCP_SORT_OPTIONS.map((opt) => (
|
|
395
|
-
<SelectItem key={opt.value} value={opt.value}>
|
|
396
|
-
{opt.label}
|
|
397
|
-
</SelectItem>
|
|
398
|
-
))}
|
|
399
|
-
</SelectContent>
|
|
400
|
-
</Select>
|
|
401
|
-
</div>
|
|
402
|
-
|
|
403
|
-
{/* Warnings */}
|
|
404
|
-
{warnings.length > 0 && (
|
|
405
|
-
<div className="flex flex-col gap-1">
|
|
406
|
-
{warnings.map((w, i) => (
|
|
407
|
-
<div
|
|
408
|
-
key={i}
|
|
409
|
-
className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-400"
|
|
410
|
-
>
|
|
411
|
-
<AlertTriangle className="size-3.5 shrink-0" />
|
|
412
|
-
{w}
|
|
413
|
-
</div>
|
|
414
|
-
))}
|
|
415
|
-
</div>
|
|
416
|
-
)}
|
|
417
|
-
|
|
418
|
-
{/* Skeleton loading */}
|
|
419
|
-
{isLoading && displayServers.length === 0 && (
|
|
420
|
-
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
421
|
-
{Array.from({ length: 6 }, (_, i) => (
|
|
422
|
-
<div
|
|
423
|
-
key={i}
|
|
424
|
-
className="skeleton-card flex flex-col gap-2 rounded-lg border border-border p-4"
|
|
425
|
-
style={{ animationDelay: `${i * 80}ms` }}
|
|
426
|
-
>
|
|
427
|
-
<div className="flex items-start gap-2.5">
|
|
428
|
-
<div className="size-9 rounded-lg bg-surface-raised" />
|
|
429
|
-
<div className="flex-1 space-y-1.5">
|
|
430
|
-
<div className="h-4 w-32 rounded bg-surface-raised" />
|
|
431
|
-
<div className="h-3 w-16 rounded-full bg-surface-raised" />
|
|
432
|
-
</div>
|
|
433
|
-
</div>
|
|
434
|
-
<div className="space-y-1.5">
|
|
435
|
-
<div className="h-3 w-full rounded bg-surface-raised" />
|
|
436
|
-
<div className="h-3 w-2/3 rounded bg-surface-raised" />
|
|
437
|
-
</div>
|
|
438
|
-
<div className="flex items-center justify-between">
|
|
439
|
-
<div className="h-5 w-12 rounded-full bg-surface-raised" />
|
|
440
|
-
<div className="h-7 w-16 rounded bg-surface-raised" />
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
))}
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
446
|
-
|
|
447
|
-
{browseError && !isSearching && (
|
|
448
|
-
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
|
|
449
|
-
{browseError}
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
|
|
453
|
-
{mcpDiagnosticsError &&
|
|
454
|
-
(mcpDiagnosticsError.includes(CLI_NOT_FOUND_MARKER) ? (
|
|
455
|
-
<div className="flex items-start gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
|
|
456
|
-
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
|
|
457
|
-
<div>
|
|
458
|
-
<p className="text-sm font-medium text-amber-300">
|
|
459
|
-
{cliStatus?.flavor === 'agent_teams_orchestrator'
|
|
460
|
-
? `${runtimeLabel} not available`
|
|
461
|
-
: `${runtimeLabel} not installed`}
|
|
462
|
-
</p>
|
|
463
|
-
<p className="mt-0.5 text-xs text-text-muted">
|
|
464
|
-
MCP health checks require {runtimeLabel}. Go to the Dashboard to install or repair
|
|
465
|
-
it.
|
|
466
|
-
</p>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
) : (
|
|
470
|
-
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
|
|
471
|
-
{mcpDiagnosticsError}
|
|
472
|
-
</div>
|
|
473
|
-
))}
|
|
474
|
-
|
|
475
|
-
{/* Installed servers (catalog-style cards for custom installs) */}
|
|
476
|
-
{installedServers.length > 0 && (
|
|
477
|
-
<div className="space-y-3">
|
|
478
|
-
<div className="flex items-center justify-between gap-3">
|
|
479
|
-
<p className="text-sm font-medium text-text">已安装</p>
|
|
480
|
-
<Badge variant="secondary" className="font-normal">
|
|
481
|
-
{installedServers.length}
|
|
482
|
-
</Badge>
|
|
483
|
-
</div>
|
|
484
|
-
<div className="mcp-servers-grid grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
485
|
-
{installedServers.map((entry) => {
|
|
486
|
-
// Find matching catalog item
|
|
487
|
-
const catalogMatch = browseCatalog.find(
|
|
488
|
-
(c) => sanitizeMcpServerName(c.name) === entry.name.toLowerCase()
|
|
489
|
-
);
|
|
490
|
-
const fakeItem: McpCatalogItem = catalogMatch ?? {
|
|
491
|
-
id: `custom:${entry.name}`,
|
|
492
|
-
name: entry.name,
|
|
493
|
-
description: entry.transport === 'http' ? 'HTTP/SSE 服务器' : 'Stdio 服务器',
|
|
494
|
-
source: 'official',
|
|
495
|
-
installSpec: null,
|
|
496
|
-
envVars: [],
|
|
497
|
-
requiresAuth: false,
|
|
498
|
-
tools: [],
|
|
499
|
-
};
|
|
500
|
-
const diagnostic =
|
|
501
|
-
mcpDiagnostics[getMcpDiagnosticKey(entry.name, entry.scope)] ??
|
|
502
|
-
mcpDiagnostics[getMcpDiagnosticKey(entry.name)] ??
|
|
503
|
-
mcpDiagnostics[entry.name] ??
|
|
504
|
-
null;
|
|
505
|
-
|
|
506
|
-
return (
|
|
507
|
-
<McpServerCard
|
|
508
|
-
key={`${entry.name}-${entry.scope}`}
|
|
509
|
-
server={fakeItem}
|
|
510
|
-
isInstalled={true}
|
|
511
|
-
installedEntry={entry}
|
|
512
|
-
installedEntries={[entry]}
|
|
513
|
-
diagnostic={diagnostic}
|
|
514
|
-
diagnosticsLoading={mcpDiagnosticsLoading}
|
|
515
|
-
onClick={setSelectedMcpServerId}
|
|
516
|
-
cliStatus={cliStatus}
|
|
517
|
-
cliStatusLoading={cliStatusLoading}
|
|
518
|
-
/>
|
|
519
|
-
);
|
|
520
|
-
})}
|
|
521
|
-
</div>
|
|
522
|
-
</div>
|
|
523
|
-
)}
|
|
524
|
-
|
|
525
|
-
{/* Empty state */}
|
|
526
|
-
{!isLoading && displayServers.length === 0 && (
|
|
527
|
-
<div className="flex flex-col items-center gap-3 rounded-sm border border-dashed border-border px-8 py-16">
|
|
528
|
-
<div className="flex size-10 items-center justify-center rounded-lg border border-border bg-surface-raised">
|
|
529
|
-
{isSearching ? (
|
|
530
|
-
<Search className="size-5 text-text-muted" />
|
|
531
|
-
) : (
|
|
532
|
-
<Server className="size-5 text-text-muted" />
|
|
533
|
-
)}
|
|
534
|
-
</div>
|
|
535
|
-
<p className="text-sm text-text-secondary">
|
|
536
|
-
{isSearching ? '没有找到服务器' : '暂无 MCP 服务器'}
|
|
537
|
-
</p>
|
|
538
|
-
<p className="text-xs text-text-muted">
|
|
539
|
-
{isSearching ? '试试其他搜索词' : '稍后再回来查看新服务器'}
|
|
540
|
-
</p>
|
|
541
|
-
</div>
|
|
542
|
-
)}
|
|
543
|
-
|
|
544
|
-
{displayServers.length > 0 && (
|
|
545
|
-
<div className="mcp-servers-grid grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
546
|
-
{displayServers.map((server) => (
|
|
547
|
-
<McpServerCard
|
|
548
|
-
key={server.id}
|
|
549
|
-
server={server}
|
|
550
|
-
isInstalled={isServerInstalled(server)}
|
|
551
|
-
installedEntry={getInstalledEntry(server)}
|
|
552
|
-
installedEntries={getInstalledEntries(server)}
|
|
553
|
-
diagnostic={getDiagnostic(server)}
|
|
554
|
-
diagnosticsLoading={mcpDiagnosticsLoading}
|
|
555
|
-
onClick={setSelectedMcpServerId}
|
|
556
|
-
cliStatus={cliStatus}
|
|
557
|
-
cliStatusLoading={cliStatusLoading}
|
|
558
|
-
/>
|
|
559
|
-
))}
|
|
560
|
-
</div>
|
|
561
|
-
)}
|
|
562
|
-
|
|
563
|
-
{/* Load more for browse */}
|
|
564
|
-
{!isSearching && browseNextCursor && (
|
|
565
|
-
<div className="flex justify-center py-4">
|
|
566
|
-
<Button
|
|
567
|
-
variant="outline"
|
|
568
|
-
size="sm"
|
|
569
|
-
disabled={browseLoading}
|
|
570
|
-
onClick={() => void mcpBrowse(browseNextCursor)}
|
|
571
|
-
>
|
|
572
|
-
Load more
|
|
573
|
-
</Button>
|
|
574
|
-
</div>
|
|
575
|
-
)}
|
|
576
|
-
|
|
577
|
-
{/* Detail dialog */}
|
|
578
|
-
<McpServerDetailDialog
|
|
579
|
-
server={selectedServer}
|
|
580
|
-
isInstalled={selectedServer ? isServerInstalled(selectedServer) : false}
|
|
581
|
-
installedEntry={selectedServer ? getInstalledEntry(selectedServer) : null}
|
|
582
|
-
installedEntries={selectedServer ? getInstalledEntries(selectedServer) : []}
|
|
583
|
-
diagnostic={selectedServer ? getDiagnostic(selectedServer) : null}
|
|
584
|
-
diagnosticsLoading={mcpDiagnosticsLoading}
|
|
585
|
-
projectPath={projectPath}
|
|
586
|
-
open={selectedMcpServerId !== null}
|
|
587
|
-
onClose={() => setSelectedMcpServerId(null)}
|
|
588
|
-
cliStatus={cliStatus}
|
|
589
|
-
cliStatusLoading={cliStatusLoading}
|
|
590
|
-
/>
|
|
591
|
-
</div>
|
|
592
|
-
);
|
|
593
|
-
};
|