mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 15:31:28 +08:00
✨ feat(ai-mcp): 增强 MCP 新增指引与内置工具提示
This commit is contained in:
@@ -38,6 +38,8 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_ai_chat_readiness');
|
||||
expect(markup).toContain('排查 MCP 接入状态');
|
||||
expect(markup).toContain('inspect_mcp_setup');
|
||||
expect(markup).toContain('新增 MCP 填写指引');
|
||||
expect(markup).toContain('inspect_mcp_authoring_guide');
|
||||
expect(markup).toContain('查看当前提示与 Skills');
|
||||
expect(markup).toContain('inspect_ai_guidance');
|
||||
expect(markup).toContain('查看当前 AI 上下文');
|
||||
@@ -68,5 +70,8 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_sql_snippets');
|
||||
expect(markup).toContain('理解样例数据');
|
||||
expect(markup).toContain('preview_table_rows');
|
||||
expect(markup).toContain('参数提示');
|
||||
expect(markup).toContain('filePath');
|
||||
expect(markup).toContain('正文预览最多返回多少字符');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ import React from 'react';
|
||||
import { ToolOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry';
|
||||
import {
|
||||
BUILTIN_AI_TOOL_INFO,
|
||||
type AIBuiltinToolInfo,
|
||||
} from '../../utils/aiToolRegistry';
|
||||
|
||||
interface AIBuiltinToolsCatalogProps {
|
||||
darkMode: boolean;
|
||||
@@ -67,6 +70,11 @@ const BUILTIN_TOOL_FLOWS = [
|
||||
steps: 'inspect_mcp_setup → inspect_ai_runtime',
|
||||
description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。',
|
||||
},
|
||||
{
|
||||
title: '新增 MCP 填写指引',
|
||||
steps: 'inspect_mcp_authoring_guide → inspect_mcp_setup',
|
||||
description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再结合当前 MCP 配置现状判断应该新增哪种启动方式。',
|
||||
},
|
||||
{
|
||||
title: '查看当前提示与 Skills',
|
||||
steps: 'inspect_ai_guidance → inspect_ai_runtime',
|
||||
@@ -149,6 +157,26 @@ const BUILTIN_TOOL_FLOWS = [
|
||||
},
|
||||
];
|
||||
|
||||
const describeToolParameters = (tool: AIBuiltinToolInfo) => {
|
||||
const schema = tool.tool.function.parameters;
|
||||
const properties = schema && typeof schema === 'object' && typeof schema.properties === 'object'
|
||||
? schema.properties
|
||||
: {};
|
||||
const required = new Set(
|
||||
Array.isArray(schema?.required) ? schema.required.map((item) => String(item)) : [],
|
||||
);
|
||||
|
||||
return Object.entries(properties).map(([name, config]) => {
|
||||
const normalized = config && typeof config === 'object' ? config as Record<string, any> : {};
|
||||
return {
|
||||
name,
|
||||
required: required.has(name),
|
||||
description: typeof normalized.description === 'string' ? normalized.description : '',
|
||||
enumValues: Array.isArray(normalized.enum) ? normalized.enum.map((item) => String(item)) : [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
|
||||
darkMode,
|
||||
overlayTheme,
|
||||
@@ -178,47 +206,97 @@ export const AIBuiltinToolsCatalog: React.FC<AIBuiltinToolsCatalogProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{BUILTIN_AI_TOOL_INFO.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: '14px 16px',
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: cardBg,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 20 }}>{tool.icon}</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText, fontFamily: 'var(--gn-font-mono)' }}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginTop: 2 }}>{tool.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{BUILTIN_AI_TOOL_INFO.map((tool) => {
|
||||
const parameterDetails = describeToolParameters(tool);
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: overlayTheme.mutedText,
|
||||
lineHeight: 1.6,
|
||||
padding: '8px 12px',
|
||||
background: darkMode ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.02)',
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: cardBg,
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{tool.detail}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 20 }}>{tool.icon}</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText, fontFamily: 'var(--gn-font-mono)' }}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginTop: 2 }}>{tool.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: overlayTheme.mutedText,
|
||||
lineHeight: 1.6,
|
||||
padding: '8px 12px',
|
||||
background: darkMode ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.02)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{tool.detail}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: overlayTheme.mutedText, opacity: 0.7, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<ToolOutlined style={{ fontSize: 12 }} />
|
||||
<span>参数:</span>
|
||||
<code style={{ fontFamily: 'var(--gn-font-mono)', fontSize: 12, padding: '1px 6px', borderRadius: 4, background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)' }}>
|
||||
{tool.params}
|
||||
</code>
|
||||
</div>
|
||||
{parameterDetails.length > 0 && (
|
||||
<div style={{ marginTop: 10, display: 'grid', gap: 8 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText }}>参数提示</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{parameterDetails.map((item) => (
|
||||
<div
|
||||
key={`${tool.name}-${item.name}`}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.76)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<code style={{ fontFamily: 'var(--gn-font-mono)', fontSize: 12 }}>{item.name}</code>
|
||||
<span
|
||||
style={{
|
||||
padding: '1px 8px',
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: item.required ? '#b45309' : '#475569',
|
||||
background: item.required
|
||||
? (darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)')
|
||||
: (darkMode ? 'rgba(148,163,184,0.18)' : 'rgba(148,163,184,0.12)'),
|
||||
}}
|
||||
>
|
||||
{item.required ? '必填' : '可选'}
|
||||
</span>
|
||||
{item.enumValues.length > 0 && (
|
||||
<span style={{ fontSize: 11, color: overlayTheme.mutedText }}>
|
||||
可选值:{item.enumValues.join(' / ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>{item.description}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: overlayTheme.mutedText, opacity: 0.7, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<ToolOutlined style={{ fontSize: 12 }} />
|
||||
<span>参数:</span>
|
||||
<code style={{ fontFamily: 'var(--gn-font-mono)', fontSize: 12, padding: '1px 6px', borderRadius: 4, background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)' }}>
|
||||
{tool.params}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ describe('AIMCPServerCard', () => {
|
||||
expect(markup).toContain('小白用户可以按这个顺序填');
|
||||
expect(markup).toContain('字段速查');
|
||||
expect(markup).toContain('保存后显示给你和 AI 看的名字');
|
||||
expect(markup).toContain('示例值:');
|
||||
expect(markup).toContain('Filesystem / Browser / GitHub');
|
||||
expect(markup).toContain('server.js / --stdio / -m / your_mcp_server');
|
||||
expect(markup).toContain('当前固定为 stdio');
|
||||
expect(markup).toContain('单次工具发现或调用最多等待多久');
|
||||
expect(markup).toContain('必填');
|
||||
@@ -60,5 +63,6 @@ describe('AIMCPServerCard', () => {
|
||||
expect(markup).toContain('稍宽松 45 秒');
|
||||
expect(markup).toContain('慢启动 60 秒');
|
||||
expect(markup).toContain('node server.js --stdio');
|
||||
expect(markup).toContain('OPENAI_API_KEY=... uvx mcp-server-fetch --stdio');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,14 @@ import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import type { AIMCPServerConfig, AIMCPToolDescriptor } from '../../types';
|
||||
import { parseMCPCommandDraft } from '../../utils/mcpCommandDraft';
|
||||
import { formatMCPEnvDraft, parseMCPEnvDraft } from '../../utils/mcpEnvDraft';
|
||||
import {
|
||||
MCP_COMMAND_EXAMPLES,
|
||||
MCP_COMMAND_PARSE_EXAMPLE,
|
||||
MCP_FIELD_GUIDES,
|
||||
MCP_SERVER_FILL_STEPS,
|
||||
buildMCPLaunchPreview,
|
||||
type MCPFieldState,
|
||||
} from '../../utils/mcpServerGuidance';
|
||||
import AIMCPCommandDraftPreview from './AIMCPCommandDraftPreview';
|
||||
|
||||
interface AIMCPServerCardProps {
|
||||
@@ -34,7 +42,7 @@ const hintStyle = (mutedText: string): React.CSSProperties => ({
|
||||
lineHeight: 1.6,
|
||||
});
|
||||
|
||||
const buildFieldTone = (kind: 'required' | 'optional' | 'fixed', darkMode: boolean) => {
|
||||
const buildFieldTone = (kind: MCPFieldState, darkMode: boolean) => {
|
||||
switch (kind) {
|
||||
case 'required':
|
||||
return {
|
||||
@@ -57,90 +65,12 @@ const buildFieldTone = (kind: 'required' | 'optional' | 'fixed', darkMode: boole
|
||||
}
|
||||
};
|
||||
|
||||
const MCP_COMMAND_EXAMPLES = [
|
||||
'uvx mcp-server-fetch',
|
||||
'node server.js --stdio',
|
||||
'python -m your_mcp_server',
|
||||
];
|
||||
|
||||
const MCP_FIELD_GUIDES: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
detail: string;
|
||||
fieldState: 'required' | 'optional' | 'fixed';
|
||||
}> = [
|
||||
{
|
||||
key: 'name',
|
||||
title: '服务名称',
|
||||
summary: '保存后显示给你和 AI 看的名字。',
|
||||
detail: '按用途命名,建议写成 Browser、GitHub、Filesystem 这类一眼能认出的名字。',
|
||||
fieldState: 'required',
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
title: '启用状态',
|
||||
summary: '控制这条配置现在要不要参与工具发现和调用。',
|
||||
detail: '禁用只是不使用,不会删除下面填好的配置。',
|
||||
fieldState: 'optional',
|
||||
},
|
||||
{
|
||||
key: 'transport',
|
||||
title: '传输方式',
|
||||
summary: 'GoNavi 用什么方式和这个 MCP Server 通信。',
|
||||
detail: '当前固定为 stdio,表示本机直接启动进程并通过标准输入输出交互。',
|
||||
fieldState: 'fixed',
|
||||
},
|
||||
{
|
||||
key: 'command',
|
||||
title: '启动命令',
|
||||
summary: '只填程序名或启动器本身。',
|
||||
detail: '常见是 node、uvx、python;脚本名和 --stdio 这类内容放到参数里。',
|
||||
fieldState: 'required',
|
||||
},
|
||||
{
|
||||
key: 'args',
|
||||
title: '命令参数',
|
||||
summary: '把脚本名、模块名、开关参数拆开逐项填写。',
|
||||
detail: '例如 node server.js --stdio,要拆成 server.js 和 --stdio 两项。',
|
||||
fieldState: 'optional',
|
||||
},
|
||||
{
|
||||
key: 'env',
|
||||
title: '环境变量',
|
||||
summary: '给 MCP Server 传入 KEY=VALUE 形式的配置。',
|
||||
detail: '通常用来放 API Key、服务地址、工作目录等;每行一条,不要写 export。',
|
||||
fieldState: 'optional',
|
||||
},
|
||||
{
|
||||
key: 'timeout',
|
||||
title: '超时(秒)',
|
||||
summary: '单次工具发现或调用最多等待多久。',
|
||||
detail: '本机常规工具一般 20 秒就够,启动慢或远端链路再适当调大。',
|
||||
fieldState: 'optional',
|
||||
},
|
||||
];
|
||||
|
||||
const quoteCommandPart = (value: string): string => {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return /[\s"]/u.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
||||
};
|
||||
|
||||
const formatLaunchPreview = (command: string, args?: string[]): string =>
|
||||
[command, ...(Array.isArray(args) ? args : [])]
|
||||
.map((item) => quoteCommandPart(item))
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const MCPHelpBlock: React.FC<{
|
||||
title: string;
|
||||
description: string;
|
||||
overlayTheme: OverlayWorkbenchTheme;
|
||||
darkMode: boolean;
|
||||
fieldState: 'required' | 'optional' | 'fixed';
|
||||
fieldState: MCPFieldState;
|
||||
example?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ title, description, overlayTheme, darkMode, fieldState, example, children }) => {
|
||||
@@ -191,7 +121,7 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
}) => {
|
||||
const [rawCommandDraft, setRawCommandDraft] = React.useState('');
|
||||
const [envDraft, setEnvDraft] = React.useState(() => formatMCPEnvDraft(server.env));
|
||||
const launchPreview = formatLaunchPreview(server.command, server.args);
|
||||
const launchPreview = buildMCPLaunchPreview(server.command, server.args);
|
||||
const parsedCommandDraft = parseMCPCommandDraft(rawCommandDraft);
|
||||
const parsedEnvDraft = parseMCPEnvDraft(envDraft);
|
||||
|
||||
@@ -228,15 +158,9 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
小白用户可以按这个顺序填:先选上面的模板或粘整行命令,再确认下面的必填项,最后只在需要时补参数、环境变量和超时。
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{[
|
||||
'1. 模板 / 完整命令',
|
||||
'2. 服务名称',
|
||||
'3. 启动命令',
|
||||
'4. 命令参数(可选)',
|
||||
'5. 环境变量 / 超时(按需)',
|
||||
].map((item) => (
|
||||
{MCP_SERVER_FILL_STEPS.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
key={item.step}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: 999,
|
||||
@@ -245,7 +169,7 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(15,23,42,0.05)',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
{item.step}. {item.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -289,6 +213,13 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
</div>
|
||||
<div style={{ fontSize: 12, lineHeight: 1.6, color: overlayTheme.titleText }}>{item.summary}</div>
|
||||
<div style={hintStyle(overlayTheme.mutedText)}>{item.detail}</div>
|
||||
{item.example ? (
|
||||
<div style={hintStyle(overlayTheme.mutedText)}>
|
||||
示例值:
|
||||
{' '}
|
||||
<code style={{ fontFamily: 'var(--gn-font-mono)' }}>{item.example}</code>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -304,7 +235,7 @@ export const AIMCPServerCard: React.FC<AIMCPServerCardProps> = ({
|
||||
rows={2}
|
||||
value={rawCommandDraft}
|
||||
onChange={(event) => setRawCommandDraft(event.target.value)}
|
||||
placeholder={"直接粘贴完整命令,例如:\nOPENAI_API_KEY=... uvx mcp-server-fetch --stdio"}
|
||||
placeholder={`直接粘贴完整命令,例如:\n${MCP_COMMAND_PARSE_EXAMPLE}`}
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}`, fontFamily: 'var(--gn-font-mono)' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('AISlashCommandMenu', () => {
|
||||
expect(markup).toContain('没有匹配的快捷命令');
|
||||
expect(markup).toContain('/sql');
|
||||
expect(markup).toContain('/health');
|
||||
expect(markup).toContain('/mcpadd');
|
||||
});
|
||||
|
||||
it('renders grouped slash command entries when matches exist', () => {
|
||||
|
||||
@@ -431,6 +431,27 @@ describe('aiLocalToolExecutor', () => {
|
||||
expect(result.content).toContain('"launchCommandPreview":"gonavi-mcp-server stdio"');
|
||||
});
|
||||
|
||||
it('returns the builtin mcp authoring guide so the model can explain how to fill command, args, env, and templates', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_mcp_authoring_guide', {}),
|
||||
connections: [buildConnection()],
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"supportsWholeCommandAutoSplit":true');
|
||||
expect(result.content).toContain('"fullCommandPasteExample":"OPENAI_API_KEY=... uvx mcp-server-fetch --stdio"');
|
||||
expect(result.content).toContain('"title":"启动命令"');
|
||||
expect(result.content).toContain('"example":"node / uvx / python"');
|
||||
expect(result.content).toContain('"title":"uvx 工具"');
|
||||
expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"');
|
||||
});
|
||||
|
||||
it('returns the current ai guidance snapshot so the model can inspect active prompts and enabled skills', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_ai_guidance', {}),
|
||||
|
||||
40
frontend/src/components/ai/aiMCPAuthoringGuideInsights.ts
Normal file
40
frontend/src/components/ai/aiMCPAuthoringGuideInsights.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
MCP_AUTHORING_NOTES,
|
||||
MCP_COMMAND_EXAMPLES,
|
||||
MCP_COMMAND_PARSE_EXAMPLE,
|
||||
MCP_FIELD_GUIDES,
|
||||
MCP_SERVER_FILL_STEPS,
|
||||
buildMCPLaunchPreview,
|
||||
} from '../../utils/mcpServerGuidance';
|
||||
import { MCP_SERVER_DRAFT_TEMPLATES } from '../../utils/mcpServerTemplates';
|
||||
|
||||
export const buildMCPAuthoringGuideSnapshot = () => ({
|
||||
fullCommandPasteExample: MCP_COMMAND_PARSE_EXAMPLE,
|
||||
commandExamples: MCP_COMMAND_EXAMPLES,
|
||||
supportsWholeCommandAutoSplit: true,
|
||||
recommendedSteps: MCP_SERVER_FILL_STEPS.map((item) => ({
|
||||
step: item.step,
|
||||
title: item.title,
|
||||
detail: item.detail,
|
||||
})),
|
||||
fieldGuides: MCP_FIELD_GUIDES.map((item) => ({
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
detail: item.detail,
|
||||
example: item.example || '',
|
||||
required: item.fieldState === 'required',
|
||||
fixed: item.fieldState === 'fixed',
|
||||
})),
|
||||
templates: MCP_SERVER_DRAFT_TEMPLATES.map((template) => ({
|
||||
key: template.key,
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
detail: template.detail,
|
||||
exampleLaunchPreview: buildMCPLaunchPreview(
|
||||
String(template.seed.command || ''),
|
||||
Array.isArray(template.seed.args) ? template.seed.args : [],
|
||||
),
|
||||
})),
|
||||
notes: MCP_AUTHORING_NOTES,
|
||||
});
|
||||
@@ -13,12 +13,14 @@ describe('aiSlashCommands', () => {
|
||||
expect(commands.length).toBeGreaterThan(8);
|
||||
expect(commands.some((command) => command.cmd === '/health')).toBe(true);
|
||||
expect(commands.some((command) => command.cmd === '/mcp')).toBe(true);
|
||||
expect(commands.some((command) => command.cmd === '/mcpadd')).toBe(true);
|
||||
});
|
||||
|
||||
it('supports filtering by chinese keywords in addition to command prefix', () => {
|
||||
const commands = filterAISlashCommands('体检');
|
||||
|
||||
expect(commands.map((command) => command.cmd)).toContain('/health');
|
||||
expect(commands.map((command) => command.cmd)).not.toContain('/mcpadd');
|
||||
});
|
||||
|
||||
it('groups commands by configured category order', () => {
|
||||
@@ -35,5 +37,6 @@ describe('aiSlashCommands', () => {
|
||||
expect(featured).toContain('/sql');
|
||||
expect(featured).toContain('/health');
|
||||
expect(featured).toContain('/mcp');
|
||||
expect(featured).toContain('/mcpadd');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ export const DEFAULT_AI_SLASH_COMMANDS: AISlashCommandDefinition[] = [
|
||||
{ cmd: '/index', label: '📊 索引建议', desc: '推荐最优索引方案', prompt: '请基于当前表结构和常见查询场景,推荐最优的索引方案并给出建表语句:', category: 'review', keywords: ['index', '索引', '慢查询'] },
|
||||
{ cmd: '/health', label: '🩺 AI 配置体检', desc: '调用体检探针总览当前 AI 配置', prompt: '请先调用 inspect_ai_setup_health,对当前 GoNavi AI 配置做一次完整体检,然后总结 blockers、warnings 和 nextActions。', category: 'diagnose', featured: true, keywords: ['health', '体检', 'ai配置', '探针'] },
|
||||
{ cmd: '/mcp', label: '🪛 排查 MCP 接入', desc: '检查 MCP 服务和外部客户端状态', prompt: '请先调用 inspect_mcp_setup,帮我盘点当前 MCP 服务、工具发现结果,以及 Claude Code / Codex 的接入状态。', category: 'diagnose', featured: true, keywords: ['mcp', 'codex', 'claude', '外部客户端'] },
|
||||
{ cmd: '/mcpadd', label: '🧭 新增 MCP 指引', desc: '查看 command、args、env 和模板怎么填', prompt: '请先调用 inspect_mcp_authoring_guide,再结合 inspect_mcp_setup,告诉我新增 GoNavi MCP 服务时 command、args、env、timeout 应该怎么填,以及最接近的模板应该选哪个。', category: 'diagnose', featured: true, keywords: ['mcp新增', 'command', 'args', 'env', '模板'] },
|
||||
{ cmd: '/safety', label: '🛡️ 查看写入安全', desc: '确认只读/写入边界和 allowMutating', prompt: '请先调用 inspect_ai_safety,告诉我当前 AI 和 GoNavi MCP 的写入边界、是否只读,以及 execute_sql 是否需要 allowMutating。', category: 'diagnose', keywords: ['安全', '只读', 'allowmutating', 'ddl', 'dml'] },
|
||||
{ cmd: '/activity', label: '🕘 最近 SQL 活动', desc: '总结最近执行、报错和热点', prompt: '请先调用 inspect_recent_sql_activity,帮我总结最近 SQL 活动、错误热点和主要读写类型。', category: 'diagnose', keywords: ['activity', 'sql日志', '最近执行', '报错'] },
|
||||
];
|
||||
|
||||
@@ -12,6 +12,7 @@ import { buildAIGuidanceSnapshot } from './aiPromptInsights';
|
||||
import { buildAIProviderSnapshot } from './aiProviderInsights';
|
||||
import { buildAIRuntimeSnapshot } from './aiRuntimeInsights';
|
||||
import { buildAISafetySnapshot } from './aiSafetyInsights';
|
||||
import { buildMCPAuthoringGuideSnapshot } from './aiMCPAuthoringGuideInsights';
|
||||
import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights';
|
||||
import { buildMCPSetupSnapshot } from './aiMCPInsights';
|
||||
import type {
|
||||
@@ -162,6 +163,11 @@ export async function executeAIConfigSnapshotToolCall(
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
case 'inspect_mcp_authoring_guide':
|
||||
return {
|
||||
content: JSON.stringify(buildMCPAuthoringGuideSnapshot()),
|
||||
success: true,
|
||||
};
|
||||
case 'inspect_ai_guidance':
|
||||
return {
|
||||
content: JSON.stringify(buildAIGuidanceSnapshot({
|
||||
@@ -181,6 +187,7 @@ export async function executeAIConfigSnapshotToolCall(
|
||||
inspect_ai_providers: '读取当前 AI 供应商配置失败',
|
||||
inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败',
|
||||
inspect_mcp_setup: '读取 MCP 配置状态失败',
|
||||
inspect_mcp_authoring_guide: '读取 MCP 新增填写指引失败',
|
||||
inspect_ai_guidance: '读取当前 AI 提示与技能配置失败',
|
||||
}[toolName] || '读取 AI 配置探针失败';
|
||||
return {
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
connections: [connections[0]],
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'],
|
||||
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'],
|
||||
skills,
|
||||
userPromptSettings,
|
||||
});
|
||||
@@ -81,6 +81,7 @@ describe('buildAISystemContextMessages', () => {
|
||||
expect(joined).toContain('inspect_ai_providers 读取真实供应商配置');
|
||||
expect(joined).toContain('inspect_ai_chat_readiness 读取真实发送前置状态');
|
||||
expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置');
|
||||
expect(joined).toContain('inspect_mcp_authoring_guide 读取真实新增指引和模板');
|
||||
expect(joined).toContain('inspect_ai_guidance 读取真实提示与技能配置');
|
||||
expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文');
|
||||
expect(joined).toContain('inspect_current_connection');
|
||||
|
||||
@@ -173,6 +173,19 @@ const appendMCPSetupInspectionGuidance = (
|
||||
});
|
||||
};
|
||||
|
||||
const appendMCPAuthoringInspectionGuidance = (
|
||||
messages: AISystemContextMessage[],
|
||||
availableToolNames: string[],
|
||||
) => {
|
||||
if (!availableToolNames.includes('inspect_mcp_authoring_guide')) {
|
||||
return;
|
||||
}
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: '如果用户提到“新增 MCP 不知道 command/args/env/timeout 怎么填”“给我一个 node / uvx / python 模板”“为什么启动命令不能直接填整行”,优先调用 inspect_mcp_authoring_guide 读取真实新增指引和模板,再结合 inspect_mcp_setup 判断当前配置现状,不要凭记忆口述。',
|
||||
});
|
||||
};
|
||||
|
||||
const appendAIGuidanceInspectionGuidance = (
|
||||
messages: AISystemContextMessage[],
|
||||
availableToolNames: string[],
|
||||
@@ -423,6 +436,7 @@ SELECT * FROM users WHERE status = 1;
|
||||
appendAIChatReadinessInspectionGuidance(systemMessages, availableToolNames);
|
||||
appendAIProviderInspectionGuidance(systemMessages, availableToolNames);
|
||||
appendMCPSetupInspectionGuidance(systemMessages, availableToolNames);
|
||||
appendMCPAuthoringInspectionGuidance(systemMessages, availableToolNames);
|
||||
appendAIGuidanceInspectionGuidance(systemMessages, availableToolNames);
|
||||
if (availableToolNames.includes('inspect_current_connection')) {
|
||||
systemMessages.push({
|
||||
|
||||
@@ -28,6 +28,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
|
||||
inspect_ai_providers: '读取当前 AI 供应商与模型配置',
|
||||
inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态',
|
||||
inspect_mcp_setup: '读取当前 MCP 配置状态',
|
||||
inspect_mcp_authoring_guide: '读取 MCP 新增填写指引',
|
||||
inspect_ai_guidance: '读取当前 AI 提示与技能配置',
|
||||
get_connections: '获取可用连接信息',
|
||||
get_databases: '扫描数据库列表',
|
||||
|
||||
@@ -409,6 +409,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_mcp_authoring_guide",
|
||||
icon: "🧭",
|
||||
desc: "查看新增 MCP 的填写指引",
|
||||
detail:
|
||||
"返回新增 MCP 表单里各字段的作用、推荐填写顺序、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适合用户问“command/args/env 到底怎么填”“给我一个 node / uvx / python 示例”“为什么启动命令不能整行填”时,先读这份真实接入指引。",
|
||||
params: "无参数",
|
||||
tool: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "inspect_mcp_authoring_guide",
|
||||
description:
|
||||
"读取 GoNavi 当前内置的 MCP 新增指引,包括推荐填写顺序、字段作用、常见命令示例、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适用于用户提到新增 MCP 不知道 command、args、env、timeout 怎么填,或想要一个最接近的模板时,先读取这份真实前端接入指南,不要凭记忆口述。",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inspect_ai_guidance",
|
||||
icon: "🧠",
|
||||
|
||||
@@ -31,6 +31,13 @@ describe('aiToolRegistry', () => {
|
||||
expect(info?.tool.function.description).toContain('外部客户端');
|
||||
});
|
||||
|
||||
it('registers the mcp-authoring-guide inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_authoring_guide');
|
||||
expect(info).toBeTruthy();
|
||||
expect(info?.desc).toContain('新增 MCP');
|
||||
expect(info?.tool.function.description).toContain('command、args、env、timeout');
|
||||
});
|
||||
|
||||
it('registers the ai-provider inspector as a builtin tool', () => {
|
||||
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_providers');
|
||||
expect(info).toBeTruthy();
|
||||
@@ -125,6 +132,7 @@ describe('aiToolRegistry', () => {
|
||||
expect(tools.some((item) => item.function.name === 'inspect_ai_providers')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_ai_chat_readiness')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_mcp_authoring_guide')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_ai_guidance')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true);
|
||||
expect(tools.some((item) => item.function.name === 'inspect_connection_capabilities')).toBe(true);
|
||||
|
||||
112
frontend/src/utils/mcpServerGuidance.ts
Normal file
112
frontend/src/utils/mcpServerGuidance.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
export type MCPFieldState = 'required' | 'optional' | 'fixed';
|
||||
|
||||
export interface MCPFieldGuide {
|
||||
key: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
detail: string;
|
||||
fieldState: MCPFieldState;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface MCPFillStep {
|
||||
step: string;
|
||||
title: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export const MCP_COMMAND_EXAMPLES = [
|
||||
'uvx mcp-server-fetch',
|
||||
'node server.js --stdio',
|
||||
'python -m your_mcp_server',
|
||||
];
|
||||
|
||||
export const MCP_COMMAND_PARSE_EXAMPLE = 'OPENAI_API_KEY=... uvx mcp-server-fetch --stdio';
|
||||
|
||||
export const MCP_SERVER_FILL_STEPS: MCPFillStep[] = [
|
||||
{ step: '1', title: '模板 / 完整命令', detail: '优先选最接近的模板,或先粘一整行命令让 GoNavi 自动拆分。' },
|
||||
{ step: '2', title: '服务名称', detail: '命名成 Browser、GitHub、Filesystem 这类一眼能认出的用途名。' },
|
||||
{ step: '3', title: '启动命令', detail: '这里只填程序名或启动器本身,不要把整行命令塞进去。' },
|
||||
{ step: '4', title: '命令参数', detail: '把脚本名、模块名和 --stdio 这类参数拆开逐项填写。' },
|
||||
{ step: '5', title: '环境变量 / 超时', detail: '只有在服务确实需要额外配置时再补,不需要可以留空。' },
|
||||
];
|
||||
|
||||
export const MCP_FIELD_GUIDES: MCPFieldGuide[] = [
|
||||
{
|
||||
key: 'name',
|
||||
title: '服务名称',
|
||||
summary: '保存后显示给你和 AI 看的名字。',
|
||||
detail: '按用途命名,建议写成 Browser、GitHub、Filesystem 这类一眼能认出的名字。',
|
||||
fieldState: 'required',
|
||||
example: 'Filesystem / Browser / GitHub',
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
title: '启用状态',
|
||||
summary: '控制这条配置现在要不要参与工具发现和调用。',
|
||||
detail: '禁用只是不使用,不会删除下面填好的配置。',
|
||||
fieldState: 'optional',
|
||||
example: '已启用 / 已禁用',
|
||||
},
|
||||
{
|
||||
key: 'transport',
|
||||
title: '传输方式',
|
||||
summary: 'GoNavi 用什么方式和这个 MCP Server 通信。',
|
||||
detail: '当前固定为 stdio,表示本机直接启动进程并通过标准输入输出交互。',
|
||||
fieldState: 'fixed',
|
||||
example: 'stdio',
|
||||
},
|
||||
{
|
||||
key: 'command',
|
||||
title: '启动命令',
|
||||
summary: '只填程序名或启动器本身。',
|
||||
detail: '常见是 node、uvx、python;脚本名和 --stdio 这类内容放到参数里。',
|
||||
fieldState: 'required',
|
||||
example: 'node / uvx / python',
|
||||
},
|
||||
{
|
||||
key: 'args',
|
||||
title: '命令参数',
|
||||
summary: '把脚本名、模块名、开关参数拆开逐项填写。',
|
||||
detail: '例如 node server.js --stdio,要拆成 server.js 和 --stdio 两项。',
|
||||
fieldState: 'optional',
|
||||
example: 'server.js / --stdio / -m / your_mcp_server',
|
||||
},
|
||||
{
|
||||
key: 'env',
|
||||
title: '环境变量',
|
||||
summary: '给 MCP Server 传入 KEY=VALUE 形式的配置。',
|
||||
detail: '通常用来放 API Key、服务地址、工作目录等;每行一条,不要写 export。',
|
||||
fieldState: 'optional',
|
||||
example: 'OPENAI_API_KEY=... / GITHUB_TOKEN=...',
|
||||
},
|
||||
{
|
||||
key: 'timeout',
|
||||
title: '超时(秒)',
|
||||
summary: '单次工具发现或调用最多等待多久。',
|
||||
detail: '本机常规工具一般 20 秒就够,启动慢或远端链路再适当调大。',
|
||||
fieldState: 'optional',
|
||||
example: '20 / 45 / 60',
|
||||
},
|
||||
];
|
||||
|
||||
export const MCP_AUTHORING_NOTES = [
|
||||
'启动命令只填程序本身,不要把脚本名、模块名和 --stdio 混进去。',
|
||||
'如果 README 里只给了一整行命令,优先粘到完整命令框自动拆分。',
|
||||
'环境变量每行一条 KEY=VALUE,不要写 export,也不要和启动命令混成一行保存。',
|
||||
'测试工具发现只会临时启动一次做探测,不会自动保存配置。',
|
||||
];
|
||||
|
||||
const quoteCommandPart = (value: string): string => {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return /[\s"]/u.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text;
|
||||
};
|
||||
|
||||
export const buildMCPLaunchPreview = (command: string, args?: string[]): string =>
|
||||
[command, ...(Array.isArray(args) ? args : [])]
|
||||
.map((item) => quoteCommandPart(item))
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
Reference in New Issue
Block a user