From 472686e8ffaa8b3c5714fef894c124a8c246ae16 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 8 Jun 2026 20:15:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-tools):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20MCP=20=E9=85=8D=E7=BD=AE=E6=8E=A2=E9=92=88=E5=B9=B6?= =?UTF-8?q?=E6=8B=86=E5=88=86=E6=9C=AC=E5=9C=B0=E6=89=A7=E8=A1=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/AIBuiltinToolsCatalog.test.tsx | 2 + .../components/ai/AIBuiltinToolsCatalog.tsx | 5 + .../components/ai/aiLocalToolExecutor.test.ts | 56 +++++ .../src/components/ai/aiLocalToolExecutor.ts | 192 ++++------------ .../src/components/ai/aiMCPInsights.test.ts | 59 +++++ frontend/src/components/ai/aiMCPInsights.ts | 93 ++++++++ .../ai/aiSnapshotInspectionToolExecutor.ts | 215 ++++++++++++++++++ .../ai/aiSystemContextMessages.test.ts | 3 +- .../components/ai/aiSystemContextMessages.ts | 14 ++ .../messageBubble/AIMessageStatusBlocks.tsx | 1 + frontend/src/utils/aiToolRegistry.test.ts | 8 + frontend/src/utils/aiToolRegistry.ts | 17 ++ 12 files changed, 515 insertions(+), 150 deletions(-) create mode 100644 frontend/src/components/ai/aiMCPInsights.test.ts create mode 100644 frontend/src/components/ai/aiMCPInsights.ts create mode 100644 frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index b9aca11..f0fabe9 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -28,6 +28,8 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('inspect_database_bundle'); expect(markup).toContain('查看 AI 当前能力'); expect(markup).toContain('inspect_ai_runtime'); + expect(markup).toContain('排查 MCP 接入状态'); + expect(markup).toContain('inspect_mcp_setup'); expect(markup).toContain('查看当前 AI 上下文'); expect(markup).toContain('inspect_ai_context'); expect(markup).toContain('查看当前连接'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index afcac52..bd7f414 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -42,6 +42,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_ai_runtime → inspect_ai_context / inspect_current_connection', description: '适合先确认当前模型、安全级别、上下文级别、Skills 和 MCP 工具,再决定让 AI 走哪条探针链路。', }, + { + title: '排查 MCP 接入状态', + steps: 'inspect_mcp_setup → inspect_ai_runtime', + description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。', + }, { title: '查看当前 AI 上下文', steps: 'inspect_ai_context → inspect_table_bundle / get_columns', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index 26edb86..4ea227c 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -224,6 +224,62 @@ describe('aiLocalToolExecutor', () => { expect(result.content).toContain('"builtinToolCount":'); }); + it('returns the current mcp setup snapshot so the model can inspect configured servers and client install state', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_mcp_setup', {}), + connections: [buildConnection()], + mcpTools: [{ + alias: 'browser_open', + originalName: 'browser_open', + serverId: 'server-1', + serverName: 'Browser', + title: '打开页面', + description: '打开页面', + }], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + getMCPServers: vi.fn().mockResolvedValue([ + { + id: 'server-1', + name: 'Browser', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-browser'], + env: { + OPENAI_API_KEY: '***', + }, + enabled: true, + timeoutSeconds: 20, + }, + ]), + getMCPClientInstallStatuses: vi.fn().mockResolvedValue([ + { + client: 'codex', + displayName: 'Codex', + installed: true, + matchesCurrent: false, + clientDetected: true, + clientCommand: 'codex', + clientPath: 'C:/Tools/codex.exe', + configPath: 'C:/Users/demo/.codex/config.toml', + command: 'gonavi-mcp-server', + args: ['stdio'], + message: '检测到旧的 GoNavi 路径', + }, + ]), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"serverCount":1'); + expect(result.content).toContain('"name":"Browser"'); + expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-browser"'); + expect(result.content).toContain('"displayName":"Codex"'); + expect(result.content).toContain('"launchCommandPreview":"gonavi-mcp-server stdio"'); + }); + it('returns the current connection snapshot so the model can inspect host, db, and ssh state', async () => { const result = await executeLocalAIToolCall({ toolCall: buildToolCall('inspect_current_connection', {}), diff --git a/frontend/src/components/ai/aiLocalToolExecutor.ts b/frontend/src/components/ai/aiLocalToolExecutor.ts index fa72540..ac8d7fe 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.ts @@ -2,12 +2,9 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/g import type { SqlLog } from '../../store'; import type { - AIContextLevel, AIChatMessage, AIContextItem, AIMCPToolDescriptor, - AIProviderConfig, - AISafetyLevel, AISkillConfig, AIToolCall, SavedConnection, @@ -15,23 +12,14 @@ import type { SqlSnippet, TabData, } from '../../types'; -import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; import { buildAIReadonlyPreviewSQL } from '../../utils/aiSqlLimit'; import { buildPaginatedSelectSQL, quoteQualifiedIdent } from '../../utils/sql'; import { resolveAITableSchemaToolResult } from '../../utils/aiTableSchemaTool'; -import { buildAIContextSnapshot } from './aiContextInsights'; -import { buildCurrentConnectionSnapshot } from './aiConnectionInsights'; -import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; import { - buildSavedQueriesSnapshot, - buildSqlSnippetsSnapshot, -} from './aiSavedSqlInsights'; -import { - buildActiveTabSnapshot, - buildRecentSqlLogsSnapshot, - buildWorkspaceTabsSnapshot, -} from './aiWorkspaceInsights'; + executeSnapshotInspectionToolCall, + type AISnapshotInspectionRuntime, +} from './aiSnapshotInspectionToolExecutor'; export interface AIToolContextEntry { connectionId: string; @@ -39,14 +27,7 @@ export interface AIToolContextEntry { tables: string[]; } -interface AILocalRuntimeState { - providers?: AIProviderConfig[]; - activeProviderId?: string; - safetyLevel?: AISafetyLevel | string; - contextLevel?: AIContextLevel | string; -} - -interface AILocalToolRuntime { +export interface AILocalToolRuntime extends AISnapshotInspectionRuntime { getDatabases: (config: any) => Promise; getTables: (config: any, dbName: string) => Promise; getAllColumns: (config: any, dbName: string) => Promise; @@ -58,7 +39,6 @@ interface AILocalToolRuntime { query: (config: any, dbName: string, sql: string) => Promise; checkSQL?: (sql: string) => Promise<{ allowed?: boolean; operationType?: string } | undefined>; callMCPTool?: (name: string, args: string) => Promise<{ content?: string; isError?: boolean } | undefined>; - getAIRuntimeState?: () => Promise; } export interface ExecuteLocalAIToolCallOptions { @@ -144,10 +124,22 @@ const buildDefaultRuntime = (): AILocalToolRuntime => ({ contextLevel: String(contextLevel || '').trim(), }; }, + getMCPServers: async () => { + const service = (window as any).go?.aiservice?.Service; + if (typeof service?.AIGetMCPServers !== 'function') { + return undefined; + } + return service.AIGetMCPServers(); + }, + getMCPClientInstallStatuses: async () => { + const service = (window as any).go?.aiservice?.Service; + if (typeof service?.AIGetMCPClientInstallStatuses !== 'function') { + return undefined; + } + return service.AIGetMCPClientInstallStatuses(); + }, }); -const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); - const normalizeTableList = (rows: any[]): string[] => rows.map((row) => row.Table || row.table || (Object.values(row)[0] as string)); @@ -235,86 +227,32 @@ export async function executeLocalAIToolCall({ try { const args = JSON.parse(toolCall.function.arguments || '{}'); + const snapshotInspectionResult = await executeSnapshotInspectionToolCall({ + toolName: toolCall.function.name, + args, + activeContext, + aiContexts, + connections, + tabs, + activeTabId, + mcpTools, + sqlLogs, + savedQueries, + sqlSnippets, + skills, + dynamicModels, + runtime: mergedRuntime, + }); + if (snapshotInspectionResult) { + content = snapshotInspectionResult.content; + success = snapshotInspectionResult.success; + return { + content, + success, + toolName: buildToolName(toolCall, descriptor), + }; + } switch (toolCall.function.name) { - case 'inspect_ai_runtime': { - try { - const runtimeState = typeof mergedRuntime.getAIRuntimeState === 'function' - ? await mergedRuntime.getAIRuntimeState() - : undefined; - content = JSON.stringify(buildAIRuntimeSnapshot({ - providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], - activeProviderId: runtimeState?.activeProviderId || '', - safetyLevel: runtimeState?.safetyLevel, - contextLevel: runtimeState?.contextLevel, - skills, - mcpTools, - dynamicModels, - builtinToolNames: BUILTIN_AI_TOOL_NAMES, - })); - success = true; - } catch (error: any) { - content = `读取当前 AI 运行状态失败: ${error?.message || error}`; - } - break; - } - case 'inspect_current_connection': { - try { - content = JSON.stringify(buildCurrentConnectionSnapshot({ - activeContext, - tabs, - activeTabId, - connections, - })); - success = true; - } catch (error: any) { - content = `读取当前连接失败: ${error?.message || error}`; - } - break; - } - case 'inspect_active_tab': { - try { - content = JSON.stringify(buildActiveTabSnapshot({ - tabs, - activeTabId, - connections, - includeContent: args.includeContent !== false, - })); - success = true; - } catch (error: any) { - content = `读取当前活动页签失败: ${error?.message || error}`; - } - break; - } - case 'inspect_workspace_tabs': { - try { - content = JSON.stringify(buildWorkspaceTabsSnapshot({ - tabs, - activeTabId, - connections, - includeContent: args.includeContent === true, - limit: args.limit, - })); - success = true; - } catch (error: any) { - content = `读取当前工作区页签失败: ${error?.message || error}`; - } - break; - } - case 'inspect_ai_context': { - try { - content = JSON.stringify(buildAIContextSnapshot({ - activeContext, - aiContexts, - connections, - includeDDL: args.includeDDL === true, - ddlLimit: args.ddlLimit, - })); - success = true; - } catch (error: any) { - content = `读取当前 AI 上下文失败: ${error?.message || error}`; - } - break; - } case 'get_connections': { const availableConnections = connections.map((connection) => ({ id: connection.id, @@ -708,50 +646,6 @@ export async function executeLocalAIToolCall({ } break; } - case 'inspect_recent_sql_logs': { - try { - content = JSON.stringify(buildRecentSqlLogsSnapshot({ - sqlLogs, - limit: args.limit, - status: args.status, - })); - success = true; - } catch (error: any) { - content = `获取最近 SQL 日志失败: ${error?.message || error}`; - } - break; - } - case 'inspect_saved_queries': { - try { - content = JSON.stringify(buildSavedQueriesSnapshot({ - savedQueries, - connections, - keyword: args.keyword, - connectionId: args.connectionId, - dbName: args.dbName, - limit: args.limit, - includeSql: args.includeSql !== false, - })); - success = true; - } catch (error: any) { - content = `读取已保存查询失败: ${error?.message || error}`; - } - break; - } - case 'inspect_sql_snippets': { - try { - content = JSON.stringify(buildSqlSnippetsSnapshot({ - sqlSnippets, - keyword: args.keyword, - limit: args.limit, - includeBody: args.includeBody !== false, - })); - success = true; - } catch (error: any) { - content = `读取 SQL 片段失败: ${error?.message || error}`; - } - break; - } case 'preview_table_rows': { const connection = findConnection(connections, args.connectionId); if (!connection) { diff --git a/frontend/src/components/ai/aiMCPInsights.test.ts b/frontend/src/components/ai/aiMCPInsights.test.ts new file mode 100644 index 0000000..31fbed9 --- /dev/null +++ b/frontend/src/components/ai/aiMCPInsights.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMCPSetupSnapshot } from './aiMCPInsights'; + +describe('aiMCPInsights', () => { + it('builds a combined snapshot for local mcp servers, tools, and external client install state', () => { + const snapshot = buildMCPSetupSnapshot({ + mcpServers: [ + { + id: 'server-1', + name: 'Browser', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-browser'], + env: { + OPENAI_API_KEY: '***', + BASE_URL: 'http://127.0.0.1', + }, + enabled: true, + timeoutSeconds: 20, + }, + ], + mcpClientStatuses: [ + { + client: 'claude-code', + displayName: 'Claude Code', + installed: true, + matchesCurrent: true, + clientDetected: true, + clientCommand: 'claude', + clientPath: 'C:/Tools/claude.exe', + configPath: 'C:/Users/demo/.claude/mcp.json', + command: 'gonavi-mcp-server', + args: ['stdio'], + message: '已写入当前 GoNavi 路径', + }, + ], + mcpTools: [ + { + alias: 'browser_open', + originalName: 'browser_open', + serverId: 'server-1', + serverName: 'Browser', + title: '打开页面', + }, + ], + }); + + expect(snapshot.serverCount).toBe(1); + expect(snapshot.enabledServerCount).toBe(1); + expect(snapshot.discoveredMCPToolCount).toBe(1); + expect(snapshot.servers[0].launchCommandPreview).toBe('uvx mcp-server-browser'); + expect(snapshot.servers[0].envVarCount).toBe(2); + expect(snapshot.servers[0].discoveredToolCount).toBe(1); + expect(snapshot.clients[0].displayName).toBe('Claude Code'); + expect(snapshot.clients[0].launchCommandPreview).toBe('gonavi-mcp-server stdio'); + expect(snapshot.currentClientCount).toBe(1); + }); +}); diff --git a/frontend/src/components/ai/aiMCPInsights.ts b/frontend/src/components/ai/aiMCPInsights.ts new file mode 100644 index 0000000..1ee3354 --- /dev/null +++ b/frontend/src/components/ai/aiMCPInsights.ts @@ -0,0 +1,93 @@ +import type { AIMCPClientInstallStatus, AIMCPServerConfig, AIMCPToolDescriptor } from '../../types'; + +const SERVER_TOOL_PREVIEW_LIMIT = 20; + +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 => + [String(command || '').trim(), ...(Array.isArray(args) ? args : [])] + .map((item) => quoteCommandPart(String(item || '').trim())) + .filter(Boolean) + .join(' '); + +const sortByName = (items: T[]): T[] => + items.slice().sort((left, right) => String(left.name || '').localeCompare(String(right.name || ''))); + +export const buildMCPSetupSnapshot = (params: { + mcpServers?: AIMCPServerConfig[]; + mcpClientStatuses?: AIMCPClientInstallStatus[]; + mcpTools?: AIMCPToolDescriptor[]; +}) => { + const { + mcpServers = [], + mcpClientStatuses = [], + mcpTools = [], + } = params; + + const normalizedServers = sortByName( + (Array.isArray(mcpServers) ? mcpServers : []).map((server) => { + const serverTools = mcpTools + .filter((tool) => tool.serverId === server.id) + .map((tool) => ({ + alias: tool.alias, + title: tool.title || tool.originalName || tool.alias, + })); + return { + id: server.id, + name: server.name, + transport: server.transport, + enabled: server.enabled !== false, + timeoutSeconds: server.timeoutSeconds, + command: server.command, + args: Array.isArray(server.args) ? server.args : [], + launchCommandPreview: formatLaunchPreview(server.command, server.args), + envKeys: Object.keys(server.env || {}).sort(), + envVarCount: Object.keys(server.env || {}).length, + discoveredToolCount: serverTools.length, + discoveredTools: serverTools.slice(0, SERVER_TOOL_PREVIEW_LIMIT), + discoveredToolsTruncated: serverTools.length > SERVER_TOOL_PREVIEW_LIMIT, + }; + }), + ); + + const normalizedClientStatuses = (Array.isArray(mcpClientStatuses) ? mcpClientStatuses : []) + .map((status) => ({ + client: status.client, + displayName: status.displayName, + installed: status.installed, + matchesCurrent: status.matchesCurrent, + clientDetected: status.clientDetected === true, + clientCommand: status.clientCommand || '', + clientPath: status.clientPath || '', + configPath: status.configPath || '', + launchCommandPreview: formatLaunchPreview(status.command, status.args), + message: status.message || '', + })) + .sort((left, right) => left.displayName.localeCompare(right.displayName)); + + const enabledServerCount = normalizedServers.filter((server) => server.enabled).length; + const installedClientCount = normalizedClientStatuses.filter((item) => item.installed).length; + const currentClientCount = normalizedClientStatuses.filter((item) => item.matchesCurrent).length; + + return { + serverCount: normalizedServers.length, + enabledServerCount, + disabledServerCount: normalizedServers.length - enabledServerCount, + discoveredMCPToolCount: Array.isArray(mcpTools) ? mcpTools.length : 0, + servers: normalizedServers, + clientInstallCount: normalizedClientStatuses.length, + installedClientCount, + currentClientCount, + detectedClientCount: normalizedClientStatuses.filter((item) => item.clientDetected).length, + clients: normalizedClientStatuses, + message: normalizedServers.length > 0 + ? `当前共配置 ${normalizedServers.length} 个 MCP 服务,其中 ${enabledServerCount} 个已启用` + : '当前还没有配置任何 MCP 服务', + }; +}; diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts new file mode 100644 index 0000000..003540f --- /dev/null +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -0,0 +1,215 @@ +import type { + AIContextItem, + AIMCPClientInstallStatus, + AIMCPServerConfig, + AIMCPToolDescriptor, + AIProviderConfig, + AISafetyLevel, + AISkillConfig, + SavedConnection, + SavedQuery, + SqlSnippet, + TabData, +} from '../../types'; +import type { SqlLog } from '../../store'; +import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; +import { buildAIContextSnapshot } from './aiContextInsights'; +import { buildCurrentConnectionSnapshot } from './aiConnectionInsights'; +import { buildMCPSetupSnapshot } from './aiMCPInsights'; +import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; +import { + buildSavedQueriesSnapshot, + buildSqlSnippetsSnapshot, +} from './aiSavedSqlInsights'; +import { + buildActiveTabSnapshot, + buildRecentSqlLogsSnapshot, + buildWorkspaceTabsSnapshot, +} from './aiWorkspaceInsights'; + +export interface AISnapshotInspectionRuntimeState { + providers?: AIProviderConfig[]; + activeProviderId?: string; + safetyLevel?: AISafetyLevel | string; + contextLevel?: string; +} + +export interface AISnapshotInspectionRuntime { + getAIRuntimeState?: () => Promise; + getMCPServers?: () => Promise; + getMCPClientInstallStatuses?: () => Promise; +} + +interface ExecuteSnapshotInspectionToolCallOptions { + toolName: string; + args: Record; + activeContext?: { connectionId: string; dbName: string } | null; + aiContexts?: Record; + connections: SavedConnection[]; + tabs?: TabData[]; + activeTabId?: string | null; + mcpTools: AIMCPToolDescriptor[]; + sqlLogs?: SqlLog[]; + savedQueries?: SavedQuery[]; + sqlSnippets?: SqlSnippet[]; + skills?: AISkillConfig[]; + dynamicModels?: string[]; + runtime?: AISnapshotInspectionRuntime; +} + +interface SnapshotInspectionResult { + content: string; + success: boolean; +} + +const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); + +export async function executeSnapshotInspectionToolCall( + options: ExecuteSnapshotInspectionToolCallOptions, +): Promise { + const { + toolName, + args, + activeContext = null, + aiContexts = {}, + connections, + tabs = [], + activeTabId = null, + mcpTools, + sqlLogs = [], + savedQueries = [], + sqlSnippets = [], + skills = [], + dynamicModels = [], + runtime, + } = options; + + try { + switch (toolName) { + case 'inspect_ai_runtime': { + const runtimeState = typeof runtime?.getAIRuntimeState === 'function' + ? await runtime.getAIRuntimeState() + : undefined; + return { + content: JSON.stringify(buildAIRuntimeSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + safetyLevel: runtimeState?.safetyLevel, + contextLevel: runtimeState?.contextLevel, + skills, + mcpTools, + dynamicModels, + builtinToolNames: BUILTIN_AI_TOOL_NAMES, + })), + success: true, + }; + } + case 'inspect_mcp_setup': { + const [mcpServers, mcpClientInstallStatuses] = await Promise.all([ + typeof runtime?.getMCPServers === 'function' ? runtime.getMCPServers() : Promise.resolve(undefined), + typeof runtime?.getMCPClientInstallStatuses === 'function' ? runtime.getMCPClientInstallStatuses() : Promise.resolve(undefined), + ]); + return { + content: JSON.stringify(buildMCPSetupSnapshot({ + mcpServers: Array.isArray(mcpServers) ? mcpServers : [], + mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [], + mcpTools, + })), + success: true, + }; + } + case 'inspect_current_connection': + return { + content: JSON.stringify(buildCurrentConnectionSnapshot({ + activeContext, + tabs, + activeTabId, + connections, + })), + success: true, + }; + case 'inspect_active_tab': + return { + content: JSON.stringify(buildActiveTabSnapshot({ + tabs, + activeTabId, + connections, + includeContent: args.includeContent !== false, + })), + success: true, + }; + case 'inspect_workspace_tabs': + return { + content: JSON.stringify(buildWorkspaceTabsSnapshot({ + tabs, + activeTabId, + connections, + includeContent: args.includeContent === true, + limit: args.limit, + })), + success: true, + }; + case 'inspect_ai_context': + return { + content: JSON.stringify(buildAIContextSnapshot({ + activeContext, + aiContexts, + connections, + includeDDL: args.includeDDL === true, + ddlLimit: args.ddlLimit, + })), + success: true, + }; + case 'inspect_recent_sql_logs': + return { + content: JSON.stringify(buildRecentSqlLogsSnapshot({ + sqlLogs, + limit: args.limit, + status: args.status, + })), + success: true, + }; + case 'inspect_saved_queries': + return { + content: JSON.stringify(buildSavedQueriesSnapshot({ + savedQueries, + connections, + keyword: args.keyword, + connectionId: args.connectionId, + dbName: args.dbName, + limit: args.limit, + includeSql: args.includeSql !== false, + })), + success: true, + }; + case 'inspect_sql_snippets': + return { + content: JSON.stringify(buildSqlSnippetsSnapshot({ + sqlSnippets, + keyword: args.keyword, + limit: args.limit, + includeBody: args.includeBody !== false, + })), + success: true, + }; + default: + return null; + } + } catch (error: any) { + const label = { + inspect_ai_runtime: '读取当前 AI 运行状态失败', + inspect_mcp_setup: '读取 MCP 配置状态失败', + inspect_current_connection: '读取当前连接失败', + inspect_active_tab: '读取当前活动页签失败', + inspect_workspace_tabs: '读取当前工作区页签失败', + inspect_ai_context: '读取当前 AI 上下文失败', + inspect_recent_sql_logs: '获取最近 SQL 日志失败', + inspect_saved_queries: '读取已保存查询失败', + inspect_sql_snippets: '读取 SQL 片段失败', + }[toolName] || '读取本地探针快照失败'; + return { + content: `${label}: ${error?.message || error}`, + success: false, + }; + } +} diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index d7ebbac..c165d70 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_ai_runtime', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_mcp_setup', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'], skills, userPromptSettings, }); @@ -76,6 +76,7 @@ describe('buildAISystemContextMessages', () => { const joined = messages.map((message) => message.content).join('\n'); expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区'); expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态'); + expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置'); expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文'); expect(joined).toContain('inspect_current_connection'); expect(joined).toContain('inspect_saved_queries'); diff --git a/frontend/src/components/ai/aiSystemContextMessages.ts b/frontend/src/components/ai/aiSystemContextMessages.ts index 6a2e583..fb9c00f 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.ts @@ -108,6 +108,19 @@ const appendAIRuntimeInspectionGuidance = ( }); }; +const appendMCPSetupInspectionGuidance = ( + messages: AISystemContextMessage[], + availableToolNames: string[], +) => { + if (!availableToolNames.includes('inspect_mcp_setup')) { + return; + } + messages.push({ + role: 'system', + content: '如果用户提到“我现在配了哪些 MCP”“Claude/Codex 有没有接入 GoNavi MCP”“为什么外部客户端用不了”“当前 MCP 服务启用了哪些”,优先调用 inspect_mcp_setup 读取真实 MCP 配置和外部客户端接入状态,不要凭记忆猜测。', + }); +}; + const resolveDatabaseDisplayType = (config: ConnectionConfig | undefined): string => { const dbType = config?.type || 'unknown'; return dbType === 'diros' ? 'Doris' : dbType.charAt(0).toUpperCase() + dbType.slice(1); @@ -325,6 +338,7 @@ SELECT * FROM users WHERE status = 1; }); } appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames); + appendMCPSetupInspectionGuidance(systemMessages, availableToolNames); if (availableToolNames.includes('inspect_current_connection')) { systemMessages.push({ role: 'system', diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 8354fee..4907cb4 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -24,6 +24,7 @@ interface AIToolCallingBlockProps { const TOOL_ACTION_LABELS: Record = { inspect_ai_runtime: '读取当前 AI 运行状态', + inspect_mcp_setup: '读取当前 MCP 配置状态', get_connections: '获取可用连接信息', get_databases: '扫描数据库列表', get_tables: '分析表结构信息', diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index a3b8078..c287418 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -10,6 +10,13 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('当前供应商'); }); + it('registers the mcp-setup inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_setup'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('MCP 配置'); + expect(info?.tool.function.description).toContain('外部客户端'); + }); + it('registers the current-connection inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_current_connection'); expect(info).toBeTruthy(); @@ -44,6 +51,7 @@ describe('aiToolRegistry', () => { }]); expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true); diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index 7ba8866..7f46b3e 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -326,6 +326,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_mcp_setup", + icon: "🪛", + desc: "查看当前 MCP 配置与外部接入状态", + detail: + "返回当前本地配置了哪些 MCP 服务、哪些已启用、每个服务声明了什么启动命令,以及 Claude Code / Codex 这类外部客户端的写入状态与命令检测结果。适合用户问“我现在配了哪些 MCP”“为什么外部客户端还用不了”“MCP 到底写没写进去”时先读真实状态。", + params: "无参数", + tool: { + type: "function", + function: { + name: "inspect_mcp_setup", + description: + "读取当前本地 MCP 配置快照,包括 MCP 服务列表、启用状态、启动命令、环境变量 key、已发现工具,以及外部客户端的 GoNavi MCP 写入状态与本机 CLI 检测结果。适用于用户提到 MCP 服务配置、Claude/Codex 是否已接入、为什么外部客户端用不了、当前到底启用了哪些 MCP 时,先读取真实配置再回答。", + parameters: { type: "object", properties: {} }, + }, + }, + }, { name: "inspect_ai_context", icon: "🧷",