diff --git a/frontend/src/components/ai/AIMCPServerFormPanel.tsx b/frontend/src/components/ai/AIMCPServerFormPanel.tsx index fc31ba0..432ae96 100644 --- a/frontend/src/components/ai/AIMCPServerFormPanel.tsx +++ b/frontend/src/components/ai/AIMCPServerFormPanel.tsx @@ -138,7 +138,7 @@ const AIMCPServerFormPanel: React.FC = ({ )} - + = ({ +
+
常见填错现象
+
+ 如果测试失败,先按这里反查要改哪个字段;大多数问题都不是 MCP 坏了,而是命令、参数或环境变量拆错了。 +
+
+ {MCP_TROUBLESHOOTING_GUIDES.map((item) => ( +
+
{item.symptom}
+
+ 常见原因:{item.likelyCause} +
+
处理方式:{item.fix}
+ {item.example ? ( +
+ 示例: + {' '} + {item.example} +
+ ) : null} +
+ ))} +
+
+
只有一条完整命令?
diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index b6e163c..883c978 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -3,6 +3,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import AISettingsMCPSection from './AISettingsMCPSection'; +import type { AISettingsMCPSectionProps } from './AISettingsMCPSection'; import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; const flattenElementText = (node: any): string => { @@ -37,62 +38,65 @@ const findElement = (node: any, predicate: (element: any) => boolean): any => { return findElement(node.props?.children, predicate); }; +const buildMCPSectionProps = (patch: Partial = {}): AISettingsMCPSectionProps => ({ + mcpClientStatuses: [ + { + client: 'claude-code', + displayName: 'Claude Code', + installed: false, + matchesCurrent: false, + clientDetected: false, + clientCommand: 'claude', + message: '未检测到 Claude Code 用户级 GoNavi MCP 配置', + }, + { + client: 'codex', + displayName: 'Codex', + installed: false, + matchesCurrent: false, + clientDetected: true, + clientCommand: 'codex', + clientPath: 'C:/Users/mock/AppData/Roaming/npm/codex.cmd', + message: '未检测到 Codex 用户级 GoNavi MCP 配置', + }, + ], + selectedMCPClient: 'claude-code', + selectedMCPClientStatus: { + client: 'claude-code', + displayName: 'Claude Code', + installed: false, + matchesCurrent: false, + clientDetected: false, + clientCommand: 'claude', + message: '未检测到 Claude Code 用户级 GoNavi MCP 配置', + }, + selectedMCPClientCommandText: '', + mcpServers: [], + mcpTools: [], + darkMode: false, + overlayTheme: buildOverlayWorkbenchTheme(false), + cardBg: '#fff', + cardBorder: 'rgba(0,0,0,0.08)', + inputBg: '#fff', + loading: false, + mcpClientStatusLoading: false, + onSelectClient: () => {}, + onRefreshStatus: () => {}, + onCopyConfigPath: () => {}, + onCopyLaunchCommand: () => {}, + onInstallSelectedClient: () => {}, + onAddServer: () => {}, + onUpdateServerDraft: () => {}, + onTestServer: () => {}, + onSaveServer: () => {}, + onDeleteServer: () => {}, + ...patch, +}); + describe('AISettingsMCPSection', () => { it('renders the extracted MCP client installer and server management entry point', () => { const markup = renderToStaticMarkup( - {}} - onRefreshStatus={() => {}} - onCopyConfigPath={() => {}} - onCopyLaunchCommand={() => {}} - onInstallSelectedClient={() => {}} - onAddServer={() => {}} - onUpdateServerDraft={() => {}} - onTestServer={() => {}} - onSaveServer={() => {}} - onDeleteServer={() => {}} - />, + , ); expect(markup).toContain('接入外部客户端'); @@ -103,33 +107,38 @@ describe('AISettingsMCPSection', () => { expect(markup).toContain('还没有 MCP 服务'); }); + it('renders troubleshooting hints when a server draft exists', () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain('常见填错现象'); + expect(markup).toContain('测试提示找不到命令'); + expect(markup).toContain('认证失败、401 或 403'); + expect(markup).toContain('当前只支持 stdio'); + expect(markup).toContain('不要把密钥写进聊天内容'); + }); + it('seeds a new draft when a launch template is selected', () => { const onAddServer = vi.fn(); - const tree = AISettingsMCPSection({ + const tree = AISettingsMCPSection(buildMCPSectionProps({ mcpClientStatuses: [], - selectedMCPClient: 'claude-code', selectedMCPClientStatus: undefined, - selectedMCPClientCommandText: '', - mcpServers: [], - mcpTools: [], - darkMode: false, - overlayTheme: buildOverlayWorkbenchTheme(false), - cardBg: '#fff', - cardBorder: 'rgba(0,0,0,0.08)', - inputBg: '#fff', - loading: false, - mcpClientStatusLoading: false, - onSelectClient: () => {}, - onRefreshStatus: () => {}, - onCopyConfigPath: () => {}, - onCopyLaunchCommand: () => {}, - onInstallSelectedClient: () => {}, onAddServer, - onUpdateServerDraft: () => {}, - onTestServer: () => {}, - onSaveServer: () => {}, - onDeleteServer: () => {}, - }); + })); const nodeTemplateButton = findElement( tree, diff --git a/frontend/src/components/ai/AISettingsMCPSection.tsx b/frontend/src/components/ai/AISettingsMCPSection.tsx index 70f8024..9c89647 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.tsx @@ -11,7 +11,7 @@ import AIMCPServerCard from './AIMCPServerCard'; export type { MCPClientKey } from '../../utils/mcpClientInstallStatus'; -interface AISettingsMCPSectionProps { +export interface AISettingsMCPSectionProps { mcpClientStatuses: AIMCPClientInstallStatus[]; selectedMCPClient: MCPClientKey; selectedMCPClientStatus?: AIMCPClientInstallStatus; diff --git a/frontend/src/utils/mcpServerGuidance.test.ts b/frontend/src/utils/mcpServerGuidance.test.ts new file mode 100644 index 0000000..6ee4aa5 --- /dev/null +++ b/frontend/src/utils/mcpServerGuidance.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { + MCP_AUTHORING_NOTES, + MCP_TROUBLESHOOTING_GUIDES, +} from './mcpServerGuidance'; + +describe('mcpServerGuidance', () => { + it('keeps actionable troubleshooting hints for common MCP setup mistakes', () => { + const symptoms = MCP_TROUBLESHOOTING_GUIDES.map((item) => item.symptom); + const allGuidance = MCP_TROUBLESHOOTING_GUIDES + .flatMap((item) => [item.likelyCause, item.fix, item.example || '']) + .join('\n'); + + expect(symptoms).toContain('测试提示找不到命令'); + expect(symptoms).toContain('认证失败、401 或 403'); + expect(allGuidance).toContain('命令参数'); + expect(allGuidance).toContain('KEY=VALUE'); + expect(allGuidance).toContain('当前只支持 stdio'); + }); + + it('warns users to keep secrets in local env config instead of chat content', () => { + expect(MCP_AUTHORING_NOTES.join('\n')).toContain('本机配置'); + expect(MCP_AUTHORING_NOTES.join('\n')).toContain('不要把密钥写进聊天内容'); + }); +}); diff --git a/frontend/src/utils/mcpServerGuidance.ts b/frontend/src/utils/mcpServerGuidance.ts index 1a643e7..1215967 100644 --- a/frontend/src/utils/mcpServerGuidance.ts +++ b/frontend/src/utils/mcpServerGuidance.ts @@ -15,6 +15,14 @@ export interface MCPFillStep { detail: string; } +export interface MCPTroubleshootingGuide { + key: string; + symptom: string; + likelyCause: string; + fix: string; + example?: string; +} + export const MCP_COMMAND_EXAMPLES = [ 'uvx mcp-server-fetch', 'node server.js --stdio', @@ -94,9 +102,41 @@ export const MCP_AUTHORING_NOTES = [ '启动命令只填程序本身,不要把脚本名、模块名和 --stdio 混进去。', '如果 README 里只给了一整行命令,优先粘到完整命令框自动拆分。', '环境变量每行一条 KEY=VALUE,不要写 export,也不要和启动命令混成一行保存。', + '密钥类环境变量会保存到本机配置,并只在启动 MCP 进程时作为进程环境传入;不要把密钥写进聊天内容。', '测试工具发现只会临时启动一次做探测,不会自动保存配置。', ]; +export const MCP_TROUBLESHOOTING_GUIDES: MCPTroubleshootingGuide[] = [ + { + key: 'command-not-found', + symptom: '测试提示找不到命令', + likelyCause: '启动命令填了整串命令、命令没加入 PATH,或 Windows 路径里有空格但没有用真实 exe 路径。', + fix: '启动命令只填可执行程序本身;脚本名和 --stdio 放到命令参数里。命令不在 PATH 时,直接填绝对路径。', + example: 'command=node, args=server.js / --stdio', + }, + { + key: 'timeout-or-no-tools', + symptom: '测试超时或发现 0 个工具', + likelyCause: '服务启动慢、缺少 stdio 参数,或填成了只支持 HTTP/SSE 的 MCP 服务。', + fix: '先确认这个服务支持 stdio,再补齐 --stdio 等参数;启动慢时把超时调到 45 或 60 秒。', + example: 'args=--stdio, timeout=45', + }, + { + key: 'auth-failed', + symptom: '认证失败、401 或 403', + likelyCause: 'API Key、Token、服务地址等环境变量没有填,或 KEY=VALUE 格式无效。', + fix: '在环境变量里每行写一条 KEY=VALUE,不要写 export,也不要把环境变量和启动命令混到同一行保存。', + example: 'GITHUB_TOKEN=...', + }, + { + key: 'stdio-only', + symptom: 'README 只给了 URL 或 SSE 配置', + likelyCause: '这类配置通常不是本机 stdio 进程,当前 GoNavi 新增 MCP 服务暂不直接支持。', + fix: '优先找该服务的 stdio 启动方式;如果只有 HTTP/SSE,请先用官方网关或本机包装器转成 stdio。', + example: '当前只支持 stdio', + }, +]; + const quoteCommandPart = (value: string): string => { const text = String(value || '').trim(); if (!text) {