feat(mcp): 强化远程 Agent 接入配置指引

This commit is contained in:
Syngnat
2026-06-11 01:11:59 +08:00
parent b438881a50
commit d3278bb4c4
5 changed files with 188 additions and 21 deletions

View File

@@ -192,6 +192,17 @@ describe('AIMCPClientInstallPanel', () => {
expect(markup).toContain('当前已选中,将复制远程接入说明');
expect(markup).toContain('远程接入边界');
expect(markup).toContain('云端 Agent 只通过 MCP 工具读取连接摘要、库表和 DDL');
expect(markup).toContain('OpenClaw 远程 MCP 快速配置');
expect(markup).toContain('配置到云端 Agent');
expect(markup).toContain('"type": "streamable-http"');
expect(markup).toContain('"url": "https://<你的域名或隧道地址>/mcp"');
expect(markup).toContain('"Authorization": "Bearer <随机token>"');
expect(markup).toContain('Windows 启动 GoNavi MCP HTTP');
expect(markup).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(markup).toContain('独立二进制gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(markup).toContain('验证顺序');
expect(markup).toContain('安全边界');
expect(markup).toContain('数据库账号和密码仍保存在 Windows GoNavi');
expect(markup).toContain('CLI 检测:远程 Agent 不需要检测本机 openclaw 命令');
expect(markup).toContain('复制 OpenClaw 远程接入说明');
});

View File

@@ -4,6 +4,7 @@ import { CheckCircleFilled, CopyOutlined, ReloadOutlined } from '@ant-design/ico
import type { AIMCPClientInstallStatus } from '../../types';
import {
buildRemoteMCPClientQuickStart,
isMCPClientKey,
isRemoteMCPClientStatus,
type MCPClientKey,
@@ -56,6 +57,9 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
onInstall,
}) => {
const selectedIsRemoteClient = isRemoteMCPClientStatus(selectedStatus);
const remoteQuickStart = selectedIsRemoteClient
? buildRemoteMCPClientQuickStart(selectedStatus)
: null;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
@@ -290,6 +294,103 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
Windows GoNavi Agent MCP DDL使 GoNavi Streamable HTTP token
</div>
)}
{remoteQuickStart && (
<div
style={{
padding: '12px 14px',
borderRadius: 12,
border: `1px solid ${darkMode ? 'rgba(56,189,248,0.2)' : 'rgba(14,165,233,0.18)'}`,
background: darkMode ? 'rgba(14,165,233,0.06)' : 'rgba(240,249,255,0.78)',
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>
{remoteQuickStart.displayName} MCP
</div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
Agent Windows GoNavi 使 MCP URL Bearer Token
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 10 }}>
<div
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(15,23,42,0.55)' : 'rgba(255,255,255,0.78)',
}}
>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}>
Agent
</div>
<code
style={{
display: 'block',
marginTop: 8,
fontFamily: 'var(--gn-font-mono)',
fontSize: 11,
color: overlayTheme.titleText,
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
}}
>
{remoteQuickStart.configJson}
</code>
</div>
<div
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(15,23,42,0.55)' : 'rgba(255,255,255,0.78)',
}}
>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}>
Windows GoNavi MCP HTTP
</div>
<code
style={{
display: 'block',
marginTop: 8,
fontFamily: 'var(--gn-font-mono)',
fontSize: 11,
color: overlayTheme.titleText,
whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere',
}}
>
{remoteQuickStart.launchCommand}
</code>
<div style={{ marginTop: 8, fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
{remoteQuickStart.standaloneCommand}
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }}>
<div>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}></div>
<div style={{ marginTop: 5, display: 'flex', flexDirection: 'column', gap: 4 }}>
{remoteQuickStart.verificationSteps.map((item) => (
<div key={item} style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
{item}
</div>
))}
</div>
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 12, color: overlayTheme.titleText }}></div>
<div style={{ marginTop: 5, display: 'flex', flexDirection: 'column', gap: 4 }}>
{remoteQuickStart.securityNotes.map((item) => (
<div key={item} style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
{item}
</div>
))}
</div>
</div>
</div>
</div>
)}
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
CLI {selectedIsRemoteClient
? `远程 Agent 不需要检测本机 ${resolveMCPClientCommandName(selectedStatus)} 命令`

View File

@@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from 'react';
import type { AIMCPClientInstallStatus } from '../../types';
import {
buildRemoteMCPClientGuide,
buildRemoteMCPClientQuickStart,
EMPTY_MCP_CLIENT_STATUSES,
formatMCPLaunchCommand,
isRemoteMCPClientStatus,
@@ -59,7 +60,9 @@ export const useAIMCPClientInstaller = ({
[mcpClientStatuses, selectedMCPClient],
);
const selectedMCPClientCommandText = useMemo(
() => formatMCPLaunchCommand(selectedMCPClientStatus),
() => isRemoteMCPClientStatus(selectedMCPClientStatus)
? buildRemoteMCPClientQuickStart(selectedMCPClientStatus).launchCommand
: formatMCPLaunchCommand(selectedMCPClientStatus),
[selectedMCPClientStatus],
);

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import type { AIMCPClientInstallStatus } from '../types';
import {
buildRemoteMCPClientGuide,
buildRemoteMCPClientQuickStart,
EMPTY_MCP_CLIENT_STATUSES,
formatMCPLaunchCommand,
isRemoteMCPClientStatus,
@@ -138,4 +139,20 @@ describe('mcpClientInstallStatus helpers', () => {
expect(guide).toContain('"Authorization": "Bearer <随机token>"');
expect(guide).toContain('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
});
it('builds remote quick-start snippets for cloud agents without database secrets', () => {
const quickStart = buildRemoteMCPClientQuickStart({
displayName: 'OpenClaw',
});
expect(quickStart.displayName).toBe('OpenClaw');
expect(quickStart.configJson).toContain('"type": "streamable-http"');
expect(quickStart.configJson).toContain('"url": "https://<你的域名或隧道地址>/mcp"');
expect(quickStart.configJson).toContain('"Authorization": "Bearer <随机token>"');
expect(quickStart.configJson).not.toContain('password');
expect(quickStart.launchCommand).toBe('GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(quickStart.standaloneCommand).toBe('gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>');
expect(quickStart.verificationSteps.join('\n')).toContain('get_connections');
expect(quickStart.securityNotes.join('\n')).toContain('allowMutating=true');
});
});

View File

@@ -4,6 +4,18 @@ export type MCPClientKey = 'claude-code' | 'codex' | 'openclaw' | 'hermans';
const AUTO_MCP_CLIENTS = new Set<MCPClientKey>(['claude-code', 'codex']);
const REMOTE_MCP_CLIENTS = new Set<MCPClientKey>(['openclaw', 'hermans']);
const DEFAULT_REMOTE_MCP_PUBLIC_URL = 'https://<你的域名或隧道地址>/mcp';
const DEFAULT_REMOTE_MCP_LOCAL_ADDR = '127.0.0.1:8765';
const DEFAULT_REMOTE_MCP_PATH = '/mcp';
export interface RemoteMCPClientQuickStart {
displayName: string;
configJson: string;
launchCommand: string;
standaloneCommand: string;
verificationSteps: string[];
securityNotes: string[];
}
export const EMPTY_MCP_CLIENT_STATUSES: AIMCPClientInstallStatus[] = [
{
@@ -160,22 +172,9 @@ export const formatMCPLaunchCommand = (
export const buildRemoteMCPClientGuide = (
status?: Pick<AIMCPClientInstallStatus, 'displayName' | 'message'> | null,
): string => {
const displayName = String(status?.displayName || '远程 Agent').trim();
const streamableHTTPConfig = [
'{',
' "mcpServers": {',
' "gonavi": {',
' "type": "streamable-http",',
' "url": "https://<你的域名或隧道地址>/mcp",',
' "headers": {',
' "Authorization": "Bearer <随机token>"',
' }',
' }',
' }',
'}',
];
const quickStart = buildRemoteMCPClientQuickStart(status);
return [
`GoNavi MCP 远程接入说明 - ${displayName}`,
`GoNavi MCP 远程接入说明 - ${quickStart.displayName}`,
'',
'目标:',
'- 数据库连接、账号和密码继续保存在 Windows 上的 GoNavi。云端 Agent 不需要保存数据库密码。',
@@ -188,17 +187,53 @@ export const buildRemoteMCPClientGuide = (
'',
'建议接入方式:',
'1. Windows 本机保持 GoNavi 可访问,由 GoNavi 读取保存连接和系统凭据。',
'2. 在 Windows 或可信内网侧运行:GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>。',
`3. 在 ${displayName} 中添加远程 MCP Servertransport 选择 Streamable HTTPURL 填隧道/反向代理后的 /mcp 地址,并设置 Authorization: Bearer <随机token>。`,
`2. 在 Windows 或可信内网侧运行:${quickStart.launchCommand}`,
`3. 在 ${quickStart.displayName} 中添加远程 MCP Servertransport 选择 Streamable HTTPURL 填隧道/反向代理后的 /mcp 地址,并设置 Authorization: Bearer <随机token>。`,
'4. 先调用 get_connections 获取 connectionId再调用表结构工具不要把数据库 host/user/password 写进云端 Agent 配置。',
'',
'可复制配置片段(适用于支持 mcpServers JSON 的 Agent',
...streamableHTTPConfig,
...quickStart.configJson.split('\n'),
'',
'CLI / 服务启动命令:',
'GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token>',
'或设置环境变量GONAVI_MCP_HTTP_TOKEN=<随机token> 后运行 gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp',
quickStart.launchCommand,
`或设置环境变量GONAVI_MCP_HTTP_TOKEN=<随机token> 后运行 ${quickStart.standaloneCommand.replace(' --token <随机token>', '')}`,
'',
status?.message ? `当前提示:${status.message}` : '',
].filter((line, index, lines) => line || index < lines.length - 1).join('\n');
};
export const buildRemoteMCPClientQuickStart = (
status?: Pick<AIMCPClientInstallStatus, 'displayName'> | null,
): RemoteMCPClientQuickStart => {
const displayName = String(status?.displayName || '远程 Agent').trim();
const launchCommand = `GoNavi.exe mcp-server http --addr ${DEFAULT_REMOTE_MCP_LOCAL_ADDR} --path ${DEFAULT_REMOTE_MCP_PATH} --token <随机token>`;
const standaloneCommand = `gonavi-mcp-server http --addr ${DEFAULT_REMOTE_MCP_LOCAL_ADDR} --path ${DEFAULT_REMOTE_MCP_PATH} --token <随机token>`;
const configJson = JSON.stringify({
mcpServers: {
gonavi: {
type: 'streamable-http',
url: DEFAULT_REMOTE_MCP_PUBLIC_URL,
headers: {
Authorization: 'Bearer <随机token>',
},
},
},
}, null, 2);
return {
displayName,
configJson,
launchCommand,
standaloneCommand,
verificationSteps: [
'Windows 本机先访问 http://127.0.0.1:8765/healthz确认 GoNavi MCP HTTP 服务已启动。',
`${displayName} 里配置 Streamable HTTP MCPURL 指向隧道或反向代理后的 /mcp 地址。`,
'先调用 get_connections 获取 connectionId再读取 get_databases / get_tables / get_columns。',
],
securityNotes: [
'数据库账号和密码仍保存在 Windows GoNavi本段配置不要写数据库密码。',
'HTTP MCP 必须使用随机 Bearer Token并放在 HTTPS、私有网络或受控隧道后面。',
'execute_sql 仍受 GoNavi AI 安全控制约束,写操作仍必须显式传 allowMutating=true。',
],
};
};