Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions server/cmd/mcp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ func applyStandaloneDefaults(configPath string, cfg *standaloneConfig) {
if cfg.MCP.Addr == 0 {
cfg.MCP.Addr = 8889
}
if strings.TrimSpace(cfg.MCP.ListenHost) == "" {
// Default to loopback so an operator who forgets to set mcp.listen_host
// never accidentally exposes MCP tools to the public network.
cfg.MCP.ListenHost = "127.0.0.1"
}
if cfg.MCP.AuthHeader == "" {
cfg.MCP.AuthHeader = "x-token"
}
Expand Down
6 changes: 6 additions & 0 deletions server/cmd/mcp/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ mcp:
version: v1.0.0
path: /mcp
addr: 8889
# listen_host is the interface the standalone MCP binary binds to.
# Leave empty or set to 127.0.0.1 for loopback-only access (recommended).
# 0.0.0.0 / :: / * are refused at startup because MCP exposes code-gen
# and DB-execution tools. Set a specific private IP if remote access is
# required, and always put a reverse proxy in front.
listen_host: 127.0.0.1
base_url: http://127.0.0.1:8889/mcp
upstream_base_url: http://127.0.0.1:8888
auth_header: x-token
Expand Down
31 changes: 30 additions & 1 deletion server/cmd/mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@ package main

import (
"fmt"
"net"
"strconv"
"strings"

"github.com/flipped-aurora/gin-vue-admin/server/global"
mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp"
_ "go.uber.org/automaxprocs"
"go.uber.org/zap"
)

// unsafeListenHosts is the set of MCP bind hosts that the standalone MCP
// binary refuses to start on. The MCP endpoint exposes code-generation,
// database execution and route mutation tools, so binding to any public
// interface effectively grants RCE to anyone who can reach the port.
//
// Operators that deliberately want MCP reachable from outside localhost
// should pick a specific private IP (e.g. the Docker bridge or a VPN
// interface) rather than blanket 0.0.0.0 / ::.
var unsafeListenHosts = map[string]struct{}{
"0.0.0.0": {},
"::": {},
"*": {},
}

func main() {
configPath, err := loadStandaloneConfig()
if err != nil {
Expand All @@ -19,12 +36,24 @@ func main() {
panic(err)
}

addr := fmt.Sprintf(":%d", global.GVA_CONFIG.MCP.Addr)
host := strings.TrimSpace(global.GVA_CONFIG.MCP.ListenHost)
if _, bad := unsafeListenHosts[host]; bad {
panic(fmt.Errorf(
"mcp.listen_host=%q is refused: binding MCP to any public interface exposes "+
"code-generation and DB-execution tools. Set mcp.listen_host to 127.0.0.1 "+
"(loopback) or a specific private interface, and front it with a reverse proxy "+
"if remote access is required.",
host,
))
}

addr := net.JoinHostPort(host, strconv.Itoa(global.GVA_CONFIG.MCP.Addr))
server := mcpTool.NewStreamableHTTPServer()

global.GVA_LOG.Info("mcp独立服务启动",
zap.String("config", configPath),
zap.String("addr", addr),
zap.String("listen_host", host),
zap.String("path", global.GVA_CONFIG.MCP.Path),
zap.String("upstream", global.GVA_CONFIG.MCP.UpstreamBaseURL),
)
Expand Down
8 changes: 8 additions & 0 deletions server/config/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ type MCP struct {
AuthHeader string `mapstructure:"auth_header" json:"auth_header" yaml:"auth_header"`
RequestTimeout int `mapstructure:"request_timeout" json:"request_timeout" yaml:"request_timeout"`

// ListenHost is the interface the standalone MCP binary binds to.
// Leave empty to default to "127.0.0.1" (loopback only).
// Binding to "0.0.0.0" / "::" / "*" is explicitly refused because the MCP
// endpoint exposes code-generation / DB-execution tools that must not be
// reachable from the public network. Put MCP behind a reverse proxy or
// bind to a specific private interface instead.
ListenHost string `mapstructure:"listen_host" json:"listen_host" yaml:"listen_host"`

// Deprecated fields kept for backward compatibility with older configs.
SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"`
MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"`
Expand Down