mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
✨ feat(ai-mcp): 补充 MCP 服务配置排错指引
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
26
frontend/src/utils/mcpServerGuidance.test.ts
Normal file
26
frontend/src/utils/mcpServerGuidance.test.ts
Normal 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('不要把密钥写进聊天内容');
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user