diff --git a/docs/develop/mcp/connect_an_external_mcp_to_ragflow.md b/docs/develop/mcp/connect_an_external_mcp_to_ragflow.md index 6d21d5629f..62a64e24c3 100644 --- a/docs/develop/mcp/connect_an_external_mcp_to_ragflow.md +++ b/docs/develop/mcp/connect_an_external_mcp_to_ragflow.md @@ -77,10 +77,10 @@ RAGFlow keeps **Save** disabled until the current connection settings have been RAGFlow validates the endpoint before connecting: - The URL must use `http://` or `https://` and contain a host. `stdio://` is not supported. -- Every address returned by DNS must be publicly routable. Loopback, private, link-local, reserved, Docker-network, and other non-public addresses are rejected. +- By default, every address returned by DNS must be publicly routable. Loopback, private, link-local, reserved, Docker-network, and other non-public addresses are rejected. - The Go MCP client connects to the validated address directly and does not use HTTP proxy environment variables. -Consequently, endpoints such as `localhost`, `127.0.0.1`, `::1`, `10.x.x.x`, `172.16.x.x` through `172.31.x.x`, and `192.168.x.x` cannot be added through this path. `ALLOW_ANY_HOST` does not disable this validation. Publish the MCP endpoint on a public address whose DNS records all resolve to public addresses. +To connect to self-hosted MCP endpoints on private or Docker networks, set `ALLOW_ANY_HOST=true` in the RAGFlow backend environment and restart the backend. This shared setting also permits private database and connector hosts. Accepted true values are `1`, `true`, `yes`, and `on` (case-insensitive, with surrounding whitespace ignored). HTTP/HTTPS scheme validation, DNS resolution, and connection pinning remain enabled. ## Troubleshoot a failed connection diff --git a/internal/common/allow_private_host.go b/internal/common/allow_private_host.go index b8651fb753..5106187559 100644 --- a/internal/common/allow_private_host.go +++ b/internal/common/allow_private_host.go @@ -29,10 +29,10 @@ import ( // truthy value (1, true, yes, on), matching the Python host guard. // // The flag is the development opt-in documented in docker/.env. It applies -// only to host-based database and connector dials (MySQL, PostgreSQL, IMAP, -// test_db_connection, ExeSQL), including host.docker.internal. URL and Invoke -// guards do not call this, so the flag cannot turn off DNS pinning for -// user-controlled URLs. Default is off. +// to host-based database and connector dials (MySQL, PostgreSQL, IMAP, +// test_db_connection, ExeSQL), including host.docker.internal, and configured +// MCP servers. Other URL and Invoke guards do not call this. DNS pinning +// remains enabled. Default is off. func AllowConfiguredPrivateHost() bool { switch strings.ToLower(strings.TrimSpace(GetEnv(EnvAllowAnyHost))) { case "1", "true", "yes", "on": diff --git a/internal/common/environments.go b/internal/common/environments.go index 220b0d5ac9..2d5e673656 100644 --- a/internal/common/environments.go +++ b/internal/common/environments.go @@ -84,7 +84,6 @@ const ( EnvRAGFlowTTSCacheTTLSeconds = "RAGFLOW_TTS_CACHE_TTL_SECONDS" EnvRerankTokenLimitMode = "RERANK_TOKEN_LIMIT_MODE" EnvComponentExecTimeout = "COMPONENT_EXEC_TIMEOUT" - EnvMCPAllowPrivateHosts = "RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS" EnvDocEngine = "DOC_ENGINE" EnvMaxFileNumPerUser = "MAX_FILE_NUM_PER_USER" EnvMaxContentLength = "MAX_CONTENT_LENGTH" diff --git a/internal/common/http.go b/internal/common/http.go index 0ae932a920..d106e7d58a 100644 --- a/internal/common/http.go +++ b/internal/common/http.go @@ -467,9 +467,9 @@ var LookupHost = net.LookupHost // a deployment mistake. The explicit "_ForTest" suffix is the // signal that production code must never touch it. // -// Host-based database and connector dials are a separate opt-in. -// AllowConfiguredPrivateHost reads ALLOW_ANY_HOST for those dials -// only. This function stays limited to the in-memory test hook. +// Host-based database and connector dials and configured MCP servers are a +// separate opt-in. AllowConfiguredPrivateHost reads ALLOW_ANY_HOST for those +// dials only. This function stays limited to the in-memory test hook. var AllowAnyHostForTest = false // allowAnyHost reads the test-only override. Kept as a private diff --git a/internal/mcp/client/mcp_call.go b/internal/mcp/client/mcp_call.go index 2ed234ad34..1fc4a9b9bd 100644 --- a/internal/mcp/client/mcp_call.go +++ b/internal/mcp/client/mcp_call.go @@ -41,7 +41,7 @@ type CallOptions struct { HTTPClient *http.Client // AllowPrivateHosts relaxes the public-routability requirement so // self-hosted MCP servers on private/docker networks work. Callers source - // it from the deployment flag (RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS); scheme + // it from the deployment flag (ALLOW_ANY_HOST); scheme // validation, DNS resolution, and IP pinning still apply. AllowPrivateHosts bool } diff --git a/internal/mcp/client/mcp_client.go b/internal/mcp/client/mcp_client.go index ae59edb0be..baade2d04f 100644 --- a/internal/mcp/client/mcp_client.go +++ b/internal/mcp/client/mcp_client.go @@ -79,7 +79,7 @@ type FetchOptions struct { HTTPClient *http.Client // AllowPrivateHosts relaxes the public-routability requirement so // self-hosted MCP servers on private/docker networks work. Callers source - // it from the deployment flag (RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS); scheme + // it from the deployment flag (ALLOW_ANY_HOST); scheme // validation, DNS resolution, and IP pinning still apply. AllowPrivateHosts bool pinHostname string diff --git a/internal/mcp/client/mcp_ssrf.go b/internal/mcp/client/mcp_ssrf.go index 2cc63ee8c2..ef8ac399ae 100644 --- a/internal/mcp/client/mcp_ssrf.go +++ b/internal/mcp/client/mcp_ssrf.go @@ -51,7 +51,7 @@ func sameOrigin(a, b *url.URL) bool { // AssertMCPURLSafe validates an MCP server URL. It delegates to // common.AssertURLSafe by default. When allowPrivateHosts is true (a // deployment-level operator decision, sourced from -// RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS at the call site), the +// ALLOW_ANY_HOST at the call site), the // public-routability requirement is relaxed so self-hosted MCP servers on // private/docker networks work — scheme validation, DNS resolution, and // connection pinning to the resolved IP still apply. diff --git a/internal/server/config.go b/internal/server/config.go index 149d7b7e07..eb80e14c81 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -170,7 +170,7 @@ func GetConfig() *config.Config { } // MCPAllowPrivateHosts reports whether self-hosted MCP servers on private -// networks are permitted (RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS). It is nil-safe: +// networks are permitted (ALLOW_ANY_HOST). It is nil-safe: // an uninitialized global config yields false so callers fail closed instead // of panicking on a nil *config.Config. func MCPAllowPrivateHosts() bool { diff --git a/internal/server/config/environments.go b/internal/server/config/environments.go index 51e22b9c0f..8cff358f5b 100644 --- a/internal/server/config/environments.go +++ b/internal/server/config/environments.go @@ -111,7 +111,7 @@ type Environments struct { TavilyAPIKey string `mapstructure:"tavily_api_key"` QueritAPIKey string `mapstructure:"querit_api_key"` KeenableAPIURL string `mapstructure:"keenable_api_url"` // KEENABLE_API_URL - MCPAllowPrivateHosts bool `mapstructure:"mcp_allow_private_hosts"` // RAGFLOW_MCP_ALLOW_PRIVATE_HOSTS + MCPAllowPrivateHosts bool `mapstructure:"mcp_allow_private_hosts"` // ALLOW_ANY_HOST } type DefaultSuperUser struct { @@ -298,10 +298,7 @@ func (c *Config) GetEnvironments() error { c.environments.HomePath = homePath } - mcpAllowPrivateHosts := common.GetEnvSmall(common.EnvMCPAllowPrivateHosts) - if mcpAllowPrivateHosts != "" { - c.environments.MCPAllowPrivateHosts = mcpAllowPrivateHosts == "true" || mcpAllowPrivateHosts == "1" || mcpAllowPrivateHosts == "yes" - } + c.environments.MCPAllowPrivateHosts = common.AllowConfiguredPrivateHost() userProfile := common.GetEnv(common.EnvUserProfile) if userProfile != "" { diff --git a/internal/server/config/mcp_config_test.go b/internal/server/config/mcp_config_test.go index e21e8ad76e..78a9889593 100644 --- a/internal/server/config/mcp_config_test.go +++ b/internal/server/config/mcp_config_test.go @@ -2,6 +2,7 @@ package config import ( "os" + "ragflow/internal/common" "reflect" "strings" "testing" @@ -20,6 +21,27 @@ func clearMCPEnv(t *testing.T) { } } +func TestMCPAllowPrivateHostsEnvironment(t *testing.T) { + for _, tc := range []struct { + value string + want bool + }{ + {"", false}, {"0", false}, {"false", false}, {"no", false}, {"off", false}, {"invalid", false}, + {"1", true}, {"true", true}, {"yes", true}, {"on", true}, {" TRUE ", true}, + } { + t.Run(tc.value, func(t *testing.T) { + t.Setenv(common.EnvAllowAnyHost, tc.value) + var cfg Config + if err := cfg.GetEnvironments(); err != nil { + t.Fatal(err) + } + if got := cfg.GetMCPAllowPrivateHosts(); got != tc.want { + t.Fatalf("ALLOW_ANY_HOST=%q: MCPAllowPrivateHosts = %v, want %v", tc.value, got, tc.want) + } + }) + } +} + func parseMCPYAML(t *testing.T, text string) (MCPConfig, error) { t.Helper() v := viper.New()