feat(ai-tools): 新增 MCP 配置探针并拆分本地执行器

This commit is contained in:
Syngnat
2026-06-08 20:15:29 +08:00
parent dc38602d32
commit 472686e8ff
12 changed files with 515 additions and 150 deletions

View File

@@ -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('查看当前连接');

View File

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

View File

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

View File

@@ -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<any>;
getTables: (config: any, dbName: string) => Promise<any>;
getAllColumns: (config: any, dbName: string) => Promise<any>;
@@ -58,7 +39,6 @@ interface AILocalToolRuntime {
query: (config: any, dbName: string, sql: string) => Promise<any>;
checkSQL?: (sql: string) => Promise<{ allowed?: boolean; operationType?: string } | undefined>;
callMCPTool?: (name: string, args: string) => Promise<{ content?: string; isError?: boolean } | undefined>;
getAIRuntimeState?: () => Promise<AILocalRuntimeState | undefined>;
}
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) {

View File

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

View File

@@ -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 = <T extends { name?: string }>(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 服务',
};
};

View File

@@ -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<AISnapshotInspectionRuntimeState | undefined>;
getMCPServers?: () => Promise<AIMCPServerConfig[] | undefined>;
getMCPClientInstallStatuses?: () => Promise<AIMCPClientInstallStatus[] | undefined>;
}
interface ExecuteSnapshotInspectionToolCallOptions {
toolName: string;
args: Record<string, any>;
activeContext?: { connectionId: string; dbName: string } | null;
aiContexts?: Record<string, AIContextItem[]>;
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<SnapshotInspectionResult | null> {
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,
};
}
}

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ interface AIToolCallingBlockProps {
const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_ai_runtime: '读取当前 AI 运行状态',
inspect_mcp_setup: '读取当前 MCP 配置状态',
get_connections: '获取可用连接信息',
get_databases: '扫描数据库列表',
get_tables: '分析表结构信息',

View File

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

View File

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