feat(ai-mcp): 增强 MCP 新增指引与内置工具提示

This commit is contained in:
Syngnat
2026-06-09 06:33:28 +08:00
parent ae3e08d5f6
commit ee5623d290
16 changed files with 373 additions and 129 deletions

View File

@@ -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('正文预览最多返回多少字符');
});
});

View File

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

View File

@@ -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');
});
});

View File

@@ -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' }}>

View File

@@ -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', () => {

View File

@@ -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', {}),

View 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,
});

View File

@@ -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');
});
});

View File

@@ -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日志', '最近执行', '报错'] },
];

View File

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

View File

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

View File

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

View File

@@ -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: '扫描数据库列表',

View File

@@ -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: "🧠",

View File

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

View 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(' ');