feat(ai-mcp): 补充 MCP 服务配置排错指引

This commit is contained in:
Syngnat
2026-06-09 16:21:03 +08:00
parent da7559426c
commit 79094d4f3b
6 changed files with 189 additions and 77 deletions

View File

@@ -138,7 +138,7 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
</div>
)}
<AIMCPHelpBlock title="环境变量" description="每行一个 KEY=VALUE通常用于 API Key、工作目录、服务地址等配置不需要时可以留空。这里只填变量本身,不要写 export也不要把它和启动命令混成一整行。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="optional" example="OPENAI_API_KEY=...">
<AIMCPHelpBlock title="环境变量" description="每行一个 KEY=VALUE通常用于 API Key、工作目录、服务地址等配置不需要时可以留空。这里会保存到本机配置,并在启动 MCP 进程时作为环境变量传入;不要写 export也不要把密钥写进聊天内容。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="optional" example="OPENAI_API_KEY=...">
<Input.TextArea
rows={3}
value={envDraft}

View File

@@ -8,6 +8,7 @@ import {
MCP_COMMAND_PARSE_EXAMPLE,
MCP_FIELD_GUIDES,
MCP_SERVER_FILL_STEPS,
MCP_TROUBLESHOOTING_GUIDES,
} from '../../utils/mcpServerGuidance';
import AIMCPCommandDraftPreview from './AIMCPCommandDraftPreview';
import { buildMCPFieldTone, buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
@@ -117,6 +118,42 @@ const AIMCPServerGuidePanel: React.FC<AIMCPServerGuidePanelProps> = ({
</div>
</div>
<div style={{ padding: '12px 14px', borderRadius: 12, border: `1px solid ${cardBorder}`, background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.76)', display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ ...mcpLabelStyle, color: overlayTheme.titleText }}></div>
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>
MCP
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 10 }}>
{MCP_TROUBLESHOOTING_GUIDES.map((item) => (
<div
key={item.key}
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.78)',
display: 'flex',
flexDirection: 'column',
gap: 6,
}}
>
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText }}>{item.symptom}</div>
<div style={{ fontSize: 12, lineHeight: 1.6, color: overlayTheme.titleText }}>
{item.likelyCause}
</div>
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>{item.fix}</div>
{item.example ? (
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>
{' '}
<code style={{ fontFamily: 'var(--gn-font-mono)' }}>{item.example}</code>
</div>
) : null}
</div>
))}
</div>
</div>
<div style={{ padding: '12px', borderRadius: 12, border: `1px solid ${cardBorder}`, background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.76)', display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ ...mcpLabelStyle, color: overlayTheme.titleText }}></div>
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>

View File

@@ -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> = {}): 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(
<AISettingsMCPSection
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={() => {}}
/>,
<AISettingsMCPSection {...buildMCPSectionProps()} />,
);
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(
<AISettingsMCPSection
{...buildMCPSectionProps({
mcpServers: [{
id: 'mcp-local',
name: 'Local MCP',
transport: 'stdio',
command: 'node',
args: ['server.js', '--stdio'],
env: {},
enabled: true,
timeoutSeconds: 20,
}],
})}
/>,
);
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,

View File

@@ -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;

View File

@@ -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('不要把密钥写进聊天内容');
});
});

View File

@@ -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) {