package mcpserver import ( "encoding/json" "flag" "fmt" "io" "os" "strings" ) const ( defaultRemoteMCPPublicURL = "https://<你的域名或隧道地址>/mcp" defaultRemoteMCPServerID = "gonavi" defaultRemoteMCPTokenHint = "<随机token>" ) // RemoteMCPClientConfigOptions 描述给云端 Agent 生成远程 MCP 配置的参数。 type RemoteMCPClientConfigOptions struct { Client string DisplayName string URL string Token string ServerID string LocalAddr string Path string GoNaviCommand string StandaloneCommand string SchemaOnly bool } // ParseRemoteMCPClientConfigOptions 解析 remote-config 模式参数。 func ParseRemoteMCPClientConfigOptions(args []string) (RemoteMCPClientConfigOptions, error) { options := RemoteMCPClientConfigOptions{ Client: "openclaw", URL: strings.TrimSpace(os.Getenv("GONAVI_MCP_PUBLIC_URL")), Token: strings.TrimSpace(os.Getenv("GONAVI_MCP_HTTP_TOKEN")), ServerID: defaultRemoteMCPServerID, LocalAddr: strings.TrimSpace(os.Getenv("GONAVI_MCP_HTTP_ADDR")), Path: strings.TrimSpace(os.Getenv("GONAVI_MCP_HTTP_PATH")), GoNaviCommand: "GoNavi.exe", StandaloneCommand: "gonavi-mcp-server", SchemaOnly: parseBoolEnvDefault("GONAVI_MCP_SCHEMA_ONLY", true), } if options.URL == "" { options.URL = defaultRemoteMCPPublicURL } if options.Token == "" { options.Token = defaultRemoteMCPTokenHint } if options.LocalAddr == "" { options.LocalAddr = defaultStreamableHTTPAddr } if options.Path == "" { options.Path = defaultStreamableHTTPPath } fs := flag.NewFlagSet("gonavi-mcp-server remote-config", flag.ContinueOnError) fs.SetOutput(io.Discard) fs.StringVar(&options.Client, "client", options.Client, "remote MCP client name, for example openclaw or hermans") fs.StringVar(&options.URL, "url", options.URL, "public Streamable HTTP MCP URL") fs.StringVar(&options.Token, "token", options.Token, "bearer token used by the remote MCP client") fs.StringVar(&options.ServerID, "server-id", options.ServerID, "MCP server id in generated config") fs.StringVar(&options.LocalAddr, "addr", options.LocalAddr, "local HTTP listen address for GoNavi") fs.StringVar(&options.Path, "path", options.Path, "local and public MCP path") fs.StringVar(&options.GoNaviCommand, "gonavi-command", options.GoNaviCommand, "GoNavi application command on Windows") fs.StringVar(&options.StandaloneCommand, "standalone-command", options.StandaloneCommand, "standalone gonavi-mcp-server command") fs.BoolVar(&options.SchemaOnly, "schema-only", options.SchemaOnly, "generate a schema-only remote MCP launch command without execute_sql") if err := fs.Parse(args); err != nil { return RemoteMCPClientConfigOptions{}, err } if fs.NArg() > 0 { return RemoteMCPClientConfigOptions{}, fmt.Errorf("未知 remote-config 参数: %s", strings.Join(fs.Args(), " ")) } return normalizeRemoteMCPClientConfigOptions(options), nil } func normalizeRemoteMCPClientConfigOptions(options RemoteMCPClientConfigOptions) RemoteMCPClientConfigOptions { options.Client = strings.ToLower(strings.TrimSpace(options.Client)) if options.Client == "" { options.Client = "remote-agent" } options.DisplayName = remoteMCPClientDisplayName(options.Client, options.DisplayName) options.URL = strings.TrimSpace(options.URL) if options.URL == "" { options.URL = defaultRemoteMCPPublicURL } options.Token = strings.TrimSpace(options.Token) if options.Token == "" { options.Token = defaultRemoteMCPTokenHint } options.ServerID = strings.TrimSpace(options.ServerID) if options.ServerID == "" { options.ServerID = defaultRemoteMCPServerID } options.LocalAddr = strings.TrimSpace(options.LocalAddr) if options.LocalAddr == "" { options.LocalAddr = defaultStreamableHTTPAddr } options.Path = strings.TrimSpace(options.Path) if options.Path == "" { options.Path = defaultStreamableHTTPPath } if !strings.HasPrefix(options.Path, "/") { options.Path = "/" + options.Path } options.GoNaviCommand = strings.TrimSpace(options.GoNaviCommand) if options.GoNaviCommand == "" { options.GoNaviCommand = "GoNavi.exe" } options.StandaloneCommand = strings.TrimSpace(options.StandaloneCommand) if options.StandaloneCommand == "" { options.StandaloneCommand = "gonavi-mcp-server" } return options } func remoteMCPClientDisplayName(client string, fallback string) string { if trimmed := strings.TrimSpace(fallback); trimmed != "" { return trimmed } switch strings.ToLower(strings.TrimSpace(client)) { case "openclaw": return "OpenClaw" case "hermans": return "Hermans" default: return "远程 Agent" } } // RenderRemoteMCPClientConfig 生成给远程 Agent 和 Windows 本机分别使用的配置文本。 func RenderRemoteMCPClientConfig(options RemoteMCPClientConfigOptions) (string, error) { normalized := normalizeRemoteMCPClientConfigOptions(options) config := map[string]any{ "mcpServers": map[string]any{ normalized.ServerID: map[string]any{ "type": "streamable-http", "url": normalized.URL, "headers": map[string]string{ "Authorization": "Bearer " + normalized.Token, }, }, }, } configJSON, err := json.MarshalIndent(config, "", " ") if err != nil { return "", fmt.Errorf("生成远程 MCP 配置失败: %w", err) } launch := remoteMCPHTTPLaunchCommand(normalized.GoNaviCommand, true, normalized.LocalAddr, normalized.Path, normalized.Token, normalized.SchemaOnly) standalone := remoteMCPHTTPLaunchCommand(normalized.StandaloneCommand, false, normalized.LocalAddr, normalized.Path, normalized.Token, normalized.SchemaOnly) lines := []string{ fmt.Sprintf("GoNavi MCP 远程接入配置 - %s", normalized.DisplayName), "", "云端 Agent 配置(不要写数据库账号密码):", string(configJSON), "", "Windows 本机启动 GoNavi MCP HTTP:", launch, "", "独立 MCP Server 启动方式:", standalone, "", "验证顺序:", fmt.Sprintf("1. Windows 本机访问 http://%s/healthz,确认返回 ok。", normalized.LocalAddr), fmt.Sprintf("2. %s 中配置上面的 Streamable HTTP MCP,URL 指向公网/隧道后的 %s。", normalized.DisplayName, normalized.URL), "3. 先调用 get_connections 获取 connectionId,再调用 get_databases / get_tables / get_columns / get_table_ddl。", "", "安全边界:", "- 数据库连接、账号和密码继续保存在 Windows GoNavi。", "- 云端 Agent 只保存 MCP URL 和 Bearer Token。", "- 默认 schema-only 模式不会注册 execute_sql,适合只给 OpenClaw/Hermans 读取库表结构。", "- 如明确去掉 --schema-only 开放 execute_sql,它仍受 GoNavi AI 安全控制约束,写操作必须显式传 allowMutating=true。", } return strings.Join(lines, "\n") + "\n", nil } // WriteRemoteMCPClientConfig 把远程 MCP 配置写入指定输出,供 CLI 模式复用。 func WriteRemoteMCPClientConfig(w io.Writer, args []string) error { if w == nil { w = io.Discard } options, err := ParseRemoteMCPClientConfigOptions(args) if err != nil { return err } text, err := RenderRemoteMCPClientConfig(options) if err != nil { return err } _, err = io.WriteString(w, text) return err } func remoteMCPHTTPLaunchCommand(command string, appSubcommand bool, addr string, path string, token string, schemaOnly bool) string { parts := []string{ command, } if appSubcommand { parts = append(parts, "mcp-server") } parts = append(parts, "http", "--addr", addr, "--path", path, "--token", token) if schemaOnly { parts = append(parts, "--schema-only") } for index, part := range parts { parts[index] = quoteCommandPart(part) } return strings.Join(parts, " ") } func quoteCommandPart(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return `""` } if !strings.ContainsAny(trimmed, " \t\"") { return trimmed } return `"` + strings.ReplaceAll(trimmed, `"`, `\"`) + `"` }