From 1d1d8d21cd7775f2822fc5c45f4ee779351ec6d8 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 15:19:23 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai):=20=E6=96=B0=E5=A2=9E=20MC?= =?UTF-8?q?P=20=E5=B7=A5=E5=85=B7=E5=8F=82=E6=95=B0=E6=8E=A2=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/AIBuiltinToolsCatalog.test.tsx | 3 + .../components/ai/AIBuiltinToolsCatalog.tsx | 5 + ...calToolExecutor.aiConfigInspection.test.ts | 39 +++ .../ai/aiMCPToolSchemaInsights.test.ts | 99 ++++++ .../components/ai/aiMCPToolSchemaInsights.ts | 296 ++++++++++++++++++ .../src/components/ai/aiSlashCommands.test.ts | 7 + frontend/src/components/ai/aiSlashCommands.ts | 1 + ...iSnapshotInspectionAIConfigToolExecutor.ts | 16 + .../ai/aiSnapshotInspectionToolExecutor.ts | 1 + .../ai/aiSystemContextMessages.test.ts | 3 +- .../ai/aiSystemInspectionGuidance.ts | 6 + .../messageBubble/AIMessageStatusBlocks.tsx | 1 + .../src/utils/aiBuiltinInspectionToolInfo.ts | 26 ++ frontend/src/utils/aiToolRegistry.test.ts | 9 + 14 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ai/aiMCPToolSchemaInsights.test.ts create mode 100644 frontend/src/components/ai/aiMCPToolSchemaInsights.ts diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 0a340b1..c3c9c66 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -42,6 +42,9 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('inspect_mcp_setup'); expect(markup).toContain('新增 MCP 填写指引'); expect(markup).toContain('inspect_mcp_authoring_guide'); + expect(markup).toContain('查看 MCP 工具参数'); + expect(markup).toContain('inspect_mcp_tool_schema'); + expect(markup).toContain('inputSchema'); expect(markup).toContain('查看当前提示与 Skills'); expect(markup).toContain('inspect_ai_guidance'); expect(markup).toContain('查看当前 AI 上下文'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index b144723..5cf4e33 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -80,6 +80,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_mcp_authoring_guide → inspect_mcp_setup', description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再结合当前 MCP 配置现状判断应该新增哪种启动方式。', }, + { + title: '查看 MCP 工具参数', + steps: 'inspect_mcp_setup → inspect_mcp_tool_schema', + description: '适合先找到当前真实发现到的 MCP 工具 alias,再读取对应 inputSchema、必填字段、枚举和嵌套参数路径,避免调用外部 MCP 工具时乱填 arguments。', + }, { title: '查看当前提示与 Skills', steps: 'inspect_ai_guidance → inspect_ai_runtime', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts index 7f89663..4357cbb 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts @@ -323,6 +323,45 @@ describe('aiLocalToolExecutor AI config inspection tools', () => { expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"'); }); + it('returns mcp tool input schemas so the model can build arguments from discovered tool metadata', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_mcp_tool_schema', { + alias: 'github_create_issue', + }), + connections: [buildConnection()], + mcpTools: [{ + alias: 'github_create_issue', + originalName: 'create_issue', + serverId: 'github-server', + serverName: 'GitHub', + title: '创建 Issue', + description: 'Create a GitHub issue', + inputSchema: { + type: 'object', + required: ['owner', 'repo', 'title'], + properties: { + owner: { type: 'string', description: '仓库 owner' }, + repo: { type: 'string', description: '仓库名' }, + title: { type: 'string', description: 'Issue 标题' }, + state: { type: 'string', enum: ['open', 'closed'] }, + }, + }, + }], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"alias":"github_create_issue"'); + expect(result.content).toContain('"requiredParameters":["owner","repo","title"]'); + expect(result.content).toContain('"path":"state"'); + expect(result.content).toContain('"enumValues":["open","closed"]'); + expect(result.content).toContain('调用 github_create_issue 前必须提供:owner, repo, title'); + }); + 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', {}), diff --git a/frontend/src/components/ai/aiMCPToolSchemaInsights.test.ts b/frontend/src/components/ai/aiMCPToolSchemaInsights.test.ts new file mode 100644 index 0000000..6a47dba --- /dev/null +++ b/frontend/src/components/ai/aiMCPToolSchemaInsights.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMCPToolSchemaSnapshot } from './aiMCPToolSchemaInsights'; + +describe('aiMCPToolSchemaInsights', () => { + it('summarizes discovered mcp tool input schemas with required, enum, and nested parameter hints', () => { + const snapshot = buildMCPToolSchemaSnapshot({ + alias: 'github_create_issue', + mcpTools: [ + { + alias: 'github_create_issue', + originalName: 'create_issue', + serverId: 'github-server', + serverName: 'GitHub', + title: '创建 Issue', + description: 'Create a GitHub issue', + inputSchema: { + type: 'object', + required: ['owner', 'repo', 'title'], + properties: { + owner: { type: 'string', description: '仓库 owner' }, + repo: { type: 'string', description: '仓库名' }, + title: { type: 'string', description: 'Issue 标题' }, + priority: { type: 'string', enum: ['low', 'medium', 'high'], default: 'medium' }, + labels: { + type: 'array', + items: { type: 'string' }, + }, + metadata: { + type: 'object', + properties: { + milestone: { type: 'string', description: '里程碑' }, + }, + }, + }, + }, + }, + ], + }); + + expect(snapshot.matchedToolCount).toBe(1); + expect(snapshot.tools[0].alias).toBe('github_create_issue'); + expect(snapshot.tools[0].requiredParameters).toEqual(['owner', 'repo', 'title']); + expect(snapshot.tools[0].parameters.map((item) => item.path)).toContain('metadata.milestone'); + expect(snapshot.tools[0].parameters.find((item) => item.path === 'priority')?.enumValues).toEqual(['low', 'medium', 'high']); + expect(snapshot.tools[0].parameters.find((item) => item.path === 'labels')?.arrayItemType).toBe('string'); + expect(snapshot.tools[0].usageHints).toContain('调用 github_create_issue 前必须提供:owner, repo, title'); + expect(snapshot.tools[0].usageHints).toContain('priority 只能从枚举值中选择:low / medium / high'); + expect(snapshot.tools[0].inputSchema).toBeUndefined(); + }); + + it('can include the raw schema when deep debugging a mcp tool argument mismatch', () => { + const snapshot = buildMCPToolSchemaSnapshot({ + alias: 'browser_open', + includeSchema: true, + mcpTools: [ + { + alias: 'browser_open', + originalName: 'open', + serverId: 'browser-server', + serverName: 'Browser', + inputSchema: { + type: 'object', + required: ['url'], + properties: { + url: { type: 'string', description: '要打开的 URL' }, + }, + }, + }, + ], + }); + + expect(snapshot.tools[0].inputSchema).toEqual({ + type: 'object', + required: ['url'], + properties: { + url: { type: 'string', description: '要打开的 URL' }, + }, + }); + }); + + it('returns actionable warnings when no discovered mcp tool matches the query', () => { + const snapshot = buildMCPToolSchemaSnapshot({ + keyword: 'github', + mcpTools: [ + { + alias: 'browser_open', + originalName: 'open', + serverId: 'browser-server', + serverName: 'Browser', + }, + ], + }); + + expect(snapshot.matchedToolCount).toBe(0); + expect(snapshot.warnings).toContain('没有找到匹配的 MCP 工具。'); + expect(snapshot.nextActions[0]).toContain('inspect_mcp_setup'); + }); +}); diff --git a/frontend/src/components/ai/aiMCPToolSchemaInsights.ts b/frontend/src/components/ai/aiMCPToolSchemaInsights.ts new file mode 100644 index 0000000..0be6631 --- /dev/null +++ b/frontend/src/components/ai/aiMCPToolSchemaInsights.ts @@ -0,0 +1,296 @@ +import type { AIMCPToolDescriptor } from '../../types'; + +const DEFAULT_TOOL_LIMIT = 8; +const MAX_TOOL_LIMIT = 30; +const MAX_PARAMETER_HINTS = 40; +const MAX_ENUM_VALUES = 12; +const MAX_SCHEMA_DEPTH = 2; + +interface JSONSchemaRecord { + [key: string]: any; +} + +const isRecord = (value: unknown): value is JSONSchemaRecord => + Boolean(value) && typeof value === 'object' && !Array.isArray(value); + +const normalizeSearchText = (value: unknown): string => + String(value || '').trim().toLowerCase(); + +const readSchemaType = (schema: JSONSchemaRecord): string => { + const rawType = schema.type; + if (Array.isArray(rawType)) { + return rawType.map((item) => String(item)).filter(Boolean).join('|') || 'unknown'; + } + if (typeof rawType === 'string' && rawType.trim()) { + return rawType.trim(); + } + if (Array.isArray(schema.enum)) { + return 'enum'; + } + if (Array.isArray(schema.anyOf)) { + return 'anyOf'; + } + if (Array.isArray(schema.oneOf)) { + return 'oneOf'; + } + if (isRecord(schema.properties)) { + return 'object'; + } + if (isRecord(schema.items)) { + return 'array'; + } + return 'unknown'; +}; + +const readRequiredSet = (schema: JSONSchemaRecord): Set => + new Set( + Array.isArray(schema.required) + ? schema.required.map((item) => String(item)).filter(Boolean) + : [], + ); + +const readDescription = (schema: JSONSchemaRecord): string => { + const description = String(schema.description || '').trim(); + if (description) { + return description; + } + return String(schema.title || '').trim(); +}; + +const readEnumValues = (schema: JSONSchemaRecord): string[] => + Array.isArray(schema.enum) + ? schema.enum.slice(0, MAX_ENUM_VALUES).map((item) => String(item)) + : []; + +const readDefaultValue = (schema: JSONSchemaRecord): string => { + if (!Object.prototype.hasOwnProperty.call(schema, 'default')) { + return ''; + } + const value = schema.default; + if (value === null) { + return 'null'; + } + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +export interface MCPToolSchemaParameterHint { + path: string; + name: string; + required: boolean; + type: string; + description: string; + enumValues: string[]; + enumValuesTruncated: boolean; + defaultValue: string; + nestedPropertyCount: number; + arrayItemType: string; +} + +const buildParameterHints = ( + schema: JSONSchemaRecord, + pathPrefix = '', + depth = 0, +): MCPToolSchemaParameterHint[] => { + if (!isRecord(schema.properties)) { + return []; + } + + const requiredSet = readRequiredSet(schema); + const hints: MCPToolSchemaParameterHint[] = []; + Object.entries(schema.properties).forEach(([name, rawChildSchema]) => { + if (hints.length >= MAX_PARAMETER_HINTS) { + return; + } + const childSchema = isRecord(rawChildSchema) ? rawChildSchema : {}; + const path = pathPrefix ? `${pathPrefix}.${name}` : name; + const childProperties = isRecord(childSchema.properties) ? childSchema.properties : {}; + const itemSchema = isRecord(childSchema.items) ? childSchema.items : {}; + const enumValues = readEnumValues(childSchema); + hints.push({ + path, + name, + required: requiredSet.has(name), + type: readSchemaType(childSchema), + description: readDescription(childSchema), + enumValues, + enumValuesTruncated: Array.isArray(childSchema.enum) && childSchema.enum.length > MAX_ENUM_VALUES, + defaultValue: readDefaultValue(childSchema), + nestedPropertyCount: Object.keys(childProperties).length, + arrayItemType: isRecord(childSchema.items) ? readSchemaType(itemSchema) : '', + }); + + if (depth >= MAX_SCHEMA_DEPTH || hints.length >= MAX_PARAMETER_HINTS) { + return; + } + if (Object.keys(childProperties).length > 0) { + hints.push(...buildParameterHints(childSchema, path, depth + 1).slice(0, MAX_PARAMETER_HINTS - hints.length)); + return; + } + if (isRecord(itemSchema.properties)) { + hints.push(...buildParameterHints(itemSchema, `${path}[]`, depth + 1).slice(0, MAX_PARAMETER_HINTS - hints.length)); + } + }); + return hints.slice(0, MAX_PARAMETER_HINTS); +}; + +const matchesKeyword = (tool: AIMCPToolDescriptor, keyword: string): boolean => { + if (!keyword) { + return true; + } + return [ + tool.alias, + tool.originalName, + tool.title, + tool.description, + tool.serverId, + tool.serverName, + ].some((item) => normalizeSearchText(item).includes(keyword)); +}; + +const buildUsageHints = (params: { + tool: AIMCPToolDescriptor; + hasInputSchema: boolean; + parameterHints: MCPToolSchemaParameterHint[]; +}) => { + const { tool, hasInputSchema, parameterHints } = params; + const hints: string[] = []; + const requiredTopLevel = parameterHints + .filter((item) => item.required && !item.path.includes('.')) + .map((item) => item.path); + const enumHint = parameterHints.find((item) => item.enumValues.length > 0); + const nestedHint = parameterHints.find((item) => item.nestedPropertyCount > 0 || item.path.includes('.')); + + if (!hasInputSchema) { + hints.push('这个 MCP 工具没有声明 inputSchema;调用前优先查看服务 README 或先用空对象试探。'); + } + if (requiredTopLevel.length > 0) { + hints.push(`调用 ${tool.alias} 前必须提供:${requiredTopLevel.join(', ')}`); + } + if (enumHint) { + hints.push(`${enumHint.path} 只能从枚举值中选择:${enumHint.enumValues.join(' / ')}`); + } + if (nestedHint) { + hints.push('嵌套对象和数组参数必须按 JSON 结构传入,不要把对象整体写成字符串。'); + } + if (parameterHints.length > 0) { + hints.push('调用前只传 schema 中声明的字段;不确定字段含义时先向用户确认,而不是猜测。'); + } + return hints; +}; + +export const buildMCPToolSchemaSnapshot = (params: { + mcpTools?: AIMCPToolDescriptor[]; + alias?: string; + serverId?: string; + keyword?: string; + includeSchema?: boolean; + limit?: number; +}) => { + const { + mcpTools = [], + alias = '', + serverId = '', + keyword = '', + includeSchema = false, + } = params; + const normalizedAlias = normalizeSearchText(alias); + const normalizedServerId = normalizeSearchText(serverId); + const normalizedKeyword = normalizeSearchText(keyword); + const limit = Math.max(1, Math.min(MAX_TOOL_LIMIT, Number(params.limit) || DEFAULT_TOOL_LIMIT)); + const allTools = Array.isArray(mcpTools) ? mcpTools : []; + + const matchedTools = allTools + .filter((tool) => { + if (normalizedAlias) { + const aliasText = normalizeSearchText(tool.alias); + const originalText = normalizeSearchText(tool.originalName); + if (aliasText !== normalizedAlias && originalText !== normalizedAlias) { + return false; + } + } + if (normalizedServerId && normalizeSearchText(tool.serverId) !== normalizedServerId) { + return false; + } + return matchesKeyword(tool, normalizedKeyword); + }) + .sort((left, right) => { + if (normalizedAlias) { + const leftExact = normalizeSearchText(left.alias) === normalizedAlias ? 0 : 1; + const rightExact = normalizeSearchText(right.alias) === normalizedAlias ? 0 : 1; + if (leftExact !== rightExact) { + return leftExact - rightExact; + } + } + return String(left.alias || '').localeCompare(String(right.alias || '')); + }); + + const tools = matchedTools.slice(0, limit).map((tool) => { + const inputSchema = isRecord(tool.inputSchema) ? tool.inputSchema : {}; + const parameterHints = buildParameterHints(inputSchema); + const topLevelParameters = parameterHints.filter((item) => !item.path.includes('.')); + const requiredParameters = topLevelParameters + .filter((item) => item.required) + .map((item) => item.path); + const hasInputSchema = Object.keys(inputSchema).length > 0; + + return { + alias: tool.alias, + originalName: tool.originalName, + title: tool.title || tool.originalName || tool.alias, + description: tool.description || '', + serverId: tool.serverId, + serverName: tool.serverName, + hasInputSchema, + parameterCount: topLevelParameters.length, + parameterHintCount: parameterHints.length, + parameterHintsTruncated: parameterHints.length >= MAX_PARAMETER_HINTS, + requiredParameterCount: requiredParameters.length, + requiredParameters, + parameters: parameterHints, + usageHints: buildUsageHints({ tool, hasInputSchema, parameterHints }), + inputSchema: includeSchema ? inputSchema : undefined, + }; + }); + + const warnings: string[] = []; + const nextActions: string[] = []; + if (allTools.length === 0) { + warnings.push('当前没有发现任何 MCP 工具,可能还没有配置 MCP 服务,或服务测试/发现失败。'); + nextActions.push('先调用 inspect_mcp_setup 查看 MCP 服务是否启用并已发现工具。'); + } else if (matchedTools.length === 0) { + warnings.push('没有找到匹配的 MCP 工具。'); + nextActions.push('先调用 inspect_mcp_setup 查看当前实际发现到的 MCP 工具 alias,再用 alias 精确查询。'); + } else if (tools.some((tool) => !tool.hasInputSchema)) { + warnings.push('部分 MCP 工具没有声明 inputSchema,参数说明可能不完整。'); + nextActions.push('没有 schema 的工具需要回到 MCP 服务 README 或工具返回错误继续确认参数。'); + } + + return { + query: { + alias: alias || '', + serverId: serverId || '', + keyword: keyword || '', + includeSchema: includeSchema === true, + limit, + }, + totalMCPToolCount: allTools.length, + matchedToolCount: matchedTools.length, + returnedToolCount: tools.length, + toolsTruncated: matchedTools.length > tools.length, + tools, + warnings, + nextActions, + message: matchedTools.length > 0 + ? `已找到 ${matchedTools.length} 个 MCP 工具,返回 ${tools.length} 个参数 schema 摘要` + : allTools.length > 0 + ? '没有找到匹配的 MCP 工具' + : '当前还没有可用 MCP 工具 schema', + }; +}; diff --git a/frontend/src/components/ai/aiSlashCommands.test.ts b/frontend/src/components/ai/aiSlashCommands.test.ts index 57d9cd0..6605778 100644 --- a/frontend/src/components/ai/aiSlashCommands.test.ts +++ b/frontend/src/components/ai/aiSlashCommands.test.ts @@ -14,6 +14,7 @@ describe('aiSlashCommands', () => { 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); + expect(commands.some((command) => command.cmd === '/mcptool')).toBe(true); expect(commands.some((command) => command.cmd === '/connfail')).toBe(true); expect(commands.some((command) => command.cmd === '/shortcuts')).toBe(true); expect(commands.some((command) => command.cmd === '/applog')).toBe(true); @@ -47,6 +48,12 @@ describe('aiSlashCommands', () => { expect(filterAISlashCommands('/air').map((command) => command.cmd)).toContain('/airender'); }); + it('supports filtering mcp tool schema diagnostics by keyword and command prefix', () => { + expect(filterAISlashCommands('arguments').map((command) => command.cmd)).toContain('/mcptool'); + expect(filterAISlashCommands('MCP工具参数').map((command) => command.cmd)).toContain('/mcptool'); + expect(filterAISlashCommands('/mcpt').map((command) => command.cmd)).toContain('/mcptool'); + }); + it('groups commands by configured category order', () => { const groups = groupAISlashCommands(filterAISlashCommands('/')); diff --git a/frontend/src/components/ai/aiSlashCommands.ts b/frontend/src/components/ai/aiSlashCommands.ts index 21a4e2b..473bcdc 100644 --- a/frontend/src/components/ai/aiSlashCommands.ts +++ b/frontend/src/components/ai/aiSlashCommands.ts @@ -50,6 +50,7 @@ export const DEFAULT_AI_SLASH_COMMANDS: AISlashCommandDefinition[] = [ { 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: '/mcptool', label: '🧩 MCP 工具参数', desc: '查看 MCP 工具 schema 和 arguments 写法', prompt: '请先调用 inspect_mcp_setup 找到当前已发现的 MCP 工具 alias;如果我已经给了工具名或关键词,再调用 inspect_mcp_tool_schema 读取对应 inputSchema,告诉我必填参数、字段类型、枚举值、嵌套路径,以及 arguments JSON 应该怎么写。', category: 'diagnose', keywords: ['mcp工具', 'mcp工具参数', 'schema', 'arguments', '参数', '工具调用', 'inputschema'] }, { cmd: '/connfail', label: '🧯 连接失败探针', desc: '总结最近连接失败、冷却和验证异常', prompt: '请先调用 inspect_recent_connection_failures,帮我总结最近数据库连接失败、连接冷却、验证失败和 SSH 隧道异常的真实日志结论;如果已经有明确地址或类型,再结合 inspect_current_connection 或 inspect_saved_connections 继续缩小范围。', category: 'diagnose', featured: true, keywords: ['连接失败', '冷却', '验证失败', 'ssh', 'mysql'] }, { cmd: '/shortcuts', label: '⌨️ 快捷键探针', desc: '读取当前 Win/Mac 快捷键配置', prompt: '请先调用 inspect_shortcuts,告诉我当前 GoNavi 的快捷键配置,尤其是执行 SQL、切换结果区、打开 AI 面板和 AI 发送消息这些动作在当前平台和另一平台分别怎么按,是否改过默认值。', category: 'diagnose', keywords: ['快捷键', 'shortcuts', '结果区', 'mac', 'windows'] }, { cmd: '/applog', label: '🪵 应用日志', desc: '回看最近 GoNavi 应用日志', prompt: '请先调用 inspect_app_logs,帮我看最近 GoNavi 应用日志里的错误和警告;如果我提到连接失败、MCP 拉起失败、启动异常或 gonavi.log,就优先结合关键词继续筛。', category: 'diagnose', keywords: ['日志', 'gonavi.log', 'mcp报错', '连接失败', '启动异常'] }, diff --git a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts index e161655..51a32e4 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts @@ -15,6 +15,7 @@ import { buildAISafetySnapshot } from './aiSafetyInsights'; import { buildMCPAuthoringGuideSnapshot } from './aiMCPAuthoringGuideInsights'; import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; import { buildMCPSetupSnapshot } from './aiMCPInsights'; +import { buildMCPToolSchemaSnapshot } from './aiMCPToolSchemaInsights'; import type { AISnapshotInspectionRuntime, AISnapshotInspectionRuntimeState, @@ -25,6 +26,7 @@ const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); interface ExecuteAIConfigSnapshotToolCallOptions { toolName: string; + args?: Record; activeContext?: { connectionId: string; dbName: string } | null; aiContexts?: Record; connections: SavedConnection[]; @@ -57,6 +59,7 @@ export async function executeAIConfigSnapshotToolCall( ): Promise { const { toolName, + args = {}, activeContext = null, aiContexts = {}, connections, @@ -168,6 +171,18 @@ export async function executeAIConfigSnapshotToolCall( content: JSON.stringify(buildMCPAuthoringGuideSnapshot()), success: true, }; + case 'inspect_mcp_tool_schema': + return { + content: JSON.stringify(buildMCPToolSchemaSnapshot({ + mcpTools, + alias: args.alias, + serverId: args.serverId, + keyword: args.keyword, + includeSchema: args.includeSchema === true, + limit: args.limit, + })), + success: true, + }; case 'inspect_ai_guidance': return { content: JSON.stringify(buildAIGuidanceSnapshot({ @@ -188,6 +203,7 @@ export async function executeAIConfigSnapshotToolCall( inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败', inspect_mcp_setup: '读取 MCP 配置状态失败', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引失败', + inspect_mcp_tool_schema: '读取 MCP 工具参数 schema 失败', inspect_ai_guidance: '读取当前 AI 提示与技能配置失败', }[toolName] || '读取 AI 配置探针失败'; return { diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index 827ccf5..08f58cb 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -114,6 +114,7 @@ export async function executeSnapshotInspectionToolCall( const aiConfigResult = await executeAIConfigSnapshotToolCall({ toolName, + args, activeContext, aiContexts, connections, diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index a894ecf..3fabbc7 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => { connections: [connections[0]], tabs: [], activeTabId: null, - availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', '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_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', '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_mcp_tool_schema', '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_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); @@ -83,6 +83,7 @@ describe('buildAISystemContextMessages', () => { 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_mcp_tool_schema 读取真实 inputSchema'); expect(joined).toContain('inspect_ai_guidance 读取真实提示与技能配置'); expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文'); expect(joined).toContain('inspect_current_connection'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index 8fce3a4..dda9061 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -92,6 +92,12 @@ export const appendDatabaseInspectionGuidanceMessages = ( 'inspect_mcp_authoring_guide', '如果用户提到“新增 MCP 不知道 command/args/env/timeout 怎么填”“给我一个 node / uvx / python 模板”“为什么启动命令不能直接填整行”,优先调用 inspect_mcp_authoring_guide 读取真实新增指引和模板,再结合 inspect_mcp_setup 判断当前配置现状,不要凭记忆口述。', ); + appendGuidanceIfToolAvailable( + messages, + availableToolNames, + 'inspect_mcp_tool_schema', + '如果用户提到“某个 MCP 工具参数怎么填”“MCP 工具调用报参数错误”“这个 MCP tool 的 arguments JSON 怎么写”,优先调用 inspect_mcp_tool_schema 读取真实 inputSchema;如果不知道 alias,先调用 inspect_mcp_setup 找到当前发现的工具 alias。', + ); appendGuidanceIfToolAvailable( messages, availableToolNames, diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index e94e88b..25136ea 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -29,6 +29,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态', inspect_mcp_setup: '读取当前 MCP 配置状态', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引', + inspect_mcp_tool_schema: '读取 MCP 工具参数 schema', inspect_ai_guidance: '读取当前 AI 提示与技能配置', get_connections: '获取可用连接信息', get_databases: '扫描数据库列表', diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts index ca35cad..5c52298 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -145,6 +145,32 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_mcp_tool_schema", + icon: "🧩", + desc: "查看 MCP 工具参数怎么传", + detail: + "按 alias、serverId 或关键词查看当前已发现 MCP 工具的 inputSchema,返回必填参数、字段类型、枚举值、嵌套对象路径和调用前提示。适合新增 MCP 成功后,用户或 AI 不知道某个 MCP 工具到底该传哪些参数时先读真实 schema。", + params: "alias?, serverId?, keyword?, includeSchema?(默认 false), limit?(默认 8)", + tool: { + type: "function", + function: { + name: "inspect_mcp_tool_schema", + description: + "读取当前已发现 MCP 工具的参数 schema 摘要,可按 alias、serverId 或关键词过滤,并返回必填字段、类型、枚举值、嵌套参数路径和调用前提示。适用于用户问某个 MCP 工具参数怎么填、AI 准备调用外部 MCP 工具但不确定 arguments JSON 怎么写、或工具调用报参数错误时,先读取真实 inputSchema 再继续。", + parameters: { + type: "object", + properties: { + alias: { type: "string", description: "可选,按 MCP 工具 alias 精确查询,例如 github_create_issue;优先通过 inspect_mcp_setup 获取真实 alias" }, + serverId: { type: "string", description: "可选,只看某个 MCP serverId 下发现的工具" }, + keyword: { type: "string", description: "可选,按工具 alias、原始名称、标题、描述或服务名做关键词筛选" }, + includeSchema: { type: "boolean", description: "可选,是否附带完整原始 inputSchema,默认 false;需要深查复杂嵌套 schema 时再开启" }, + limit: { type: "number", description: "可选,最多返回多少个匹配工具,默认 8,最大 30" }, + }, + }, + }, + }, + }, { name: "inspect_ai_guidance", icon: "🧠", diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index b98c5cf..0638f39 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -38,6 +38,14 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('command、args、env、timeout'); }); + it('registers the mcp-tool-schema inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_tool_schema'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('MCP 工具参数'); + expect(info?.tool.function.description).toContain('inputSchema'); + expect(info?.tool.function.parameters?.properties?.alias?.description).toContain('真实 alias'); + }); + 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(); @@ -183,6 +191,7 @@ describe('aiToolRegistry', () => { 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_mcp_tool_schema')).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);