From 25b82f2544d6855680a01a233d7273dd647e9ce7 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:37:56 -0400 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20http=20=E4=BC=A0=E8=BE=93=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=B7=B3=E8=BF=87=20TLS=20=E8=AF=81=E4=B9=A6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C(=E8=87=AA=E7=AD=BE=E8=AF=81=E4=B9=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP server 走 HTTPS 且用自签证书时,客户端 TLS 校验失败,报 "tls: failed to verify certificate" 无法连接。 mcp_servers 增加 insecure 开关(默认 false),置 true 时远程 MCP 客户端使用 InsecureSkipVerify 的 transport。stdio 传输不受影响。 - mcphttp.New 增加 insecure 参数 - db: MCPServer.Insecure 字段 + schema 加列(补 ALTER 兼容旧库) - create_mcp/update_mcp 工具与前端 http 配置项增加勾选框 Closes #108 --- db/config.go | 13 +++++++------ db/schema.sql | 3 +++ mcphttp/client.go | 11 +++++++++-- server/mcpdiscover.go | 2 +- server/platform_tools.go | 8 +++++++- server/sync_scopesentry.go | 4 ++-- web/src/app/(main)/system/mcp/page.tsx | 12 ++++++++++++ web/src/lib/types.ts | 1 + 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/db/config.go b/db/config.go index 32c605b..a38f448 100644 --- a/db/config.go +++ b/db/config.go @@ -815,11 +815,12 @@ type MCPServer struct { Env json.RawMessage `json:"env"` URL string `json:"url,omitempty"` Enabled bool `json:"enabled"` + Insecure bool `json:"insecure"` // http: skip TLS cert verification (self-signed servers, issue #108) Tools []string `json:"tools,omitempty"` // cached tool names (mcp_tools_cache) } func (d *DB) ListMCP() ([]*MCPServer, error) { - rows, err := d.Query(`SELECT id,name,transport,COALESCE(command,''),args,env,COALESCE(url,''),enabled FROM mcp_servers ORDER BY id`) + rows, err := d.Query(`SELECT id,name,transport,COALESCE(command,''),args,env,COALESCE(url,''),enabled,insecure FROM mcp_servers ORDER BY id`) if err != nil { return nil, err } @@ -827,7 +828,7 @@ func (d *DB) ListMCP() ([]*MCPServer, error) { for rows.Next() { var m MCPServer var args, env []byte - if err := rows.Scan(&m.ID, &m.Name, &m.Transport, &m.Command, &args, &env, &m.URL, &m.Enabled); err != nil { + if err := rows.Scan(&m.ID, &m.Name, &m.Transport, &m.Command, &args, &env, &m.URL, &m.Enabled, &m.Insecure); err != nil { rows.Close() return nil, err } @@ -917,12 +918,12 @@ func (d *DB) SaveMCP(m *MCPServer) (int64, error) { } if m.ID == 0 { var id int64 - err := d.QueryRow(`INSERT INTO mcp_servers(name,transport,command,args,env,url,enabled) VALUES ($1,$2,NULLIF($3,''),$4,$5,NULLIF($6,''),$7) RETURNING id`, - m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled).Scan(&id) + err := d.QueryRow(`INSERT INTO mcp_servers(name,transport,command,args,env,url,enabled,insecure) VALUES ($1,$2,NULLIF($3,''),$4,$5,NULLIF($6,''),$7,$8) RETURNING id`, + m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure).Scan(&id) return id, err } - _, err := d.Exec(`UPDATE mcp_servers SET name=$1,transport=$2,command=NULLIF($3,''),args=$4,env=$5,url=NULLIF($6,''),enabled=$7 WHERE id=$8`, - m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.ID) + _, err := d.Exec(`UPDATE mcp_servers SET name=$1,transport=$2,command=NULLIF($3,''),args=$4,env=$5,url=NULLIF($6,''),enabled=$7,insecure=$8 WHERE id=$9`, + m.Name, m.Transport, m.Command, args, env, m.URL, m.Enabled, m.Insecure, m.ID) return m.ID, err } diff --git a/db/schema.sql b/db/schema.sql index e74a1e6..3b4dc2e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -825,9 +825,12 @@ CREATE TABLE IF NOT EXISTS mcp_servers ( env JSONB NOT NULL DEFAULT '{}', url TEXT, enabled BOOLEAN NOT NULL DEFAULT true, + insecure BOOLEAN NOT NULL DEFAULT false, -- http: 跳过 TLS 证书校验(自签证书场景, issue #108) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +-- 旧库补列(schema.sql 每次启动都会 Exec)。 +ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS insecure BOOLEAN NOT NULL DEFAULT false; DROP TRIGGER IF EXISTS trg_mcp_upd ON mcp_servers; CREATE TRIGGER trg_mcp_upd BEFORE UPDATE ON mcp_servers FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/mcphttp/client.go b/mcphttp/client.go index 55df1eb..345f346 100644 --- a/mcphttp/client.go +++ b/mcphttp/client.go @@ -12,6 +12,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -65,12 +66,18 @@ type Client struct { // New connects to a remote MCP endpoint and performs the initialize handshake. // headers are sent on every request (Authorization, custom API keys, …). -func New(ctx context.Context, server, url string, headers map[string]string) (*Client, error) { +// When insecure is true, TLS certificate verification is skipped so servers that +// present a self-signed certificate can still be reached (issue #108). +func New(ctx context.Context, server, url string, headers map[string]string, insecure bool) (*Client, error) { + hc := &http.Client{Timeout: 120 * time.Second} + if insecure { + hc.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + } c := &Client{ server: server, url: url, headers: headers, - http: &http.Client{Timeout: 120 * time.Second}, + http: hc, } if err := c.initialize(ctx); err != nil { return nil, err diff --git a/server/mcpdiscover.go b/server/mcpdiscover.go index 45e4d17..edf199b 100644 --- a/server/mcpdiscover.go +++ b/server/mcpdiscover.go @@ -32,7 +32,7 @@ func connectMCP(ctx context.Context, m *db.MCPServer) (mcpClient, error) { return nil, fmt.Errorf("http 传输缺少 URL") } // env map doubles as HTTP headers (e.g. Authorization). - return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env)) + return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure) default: return nil, fmt.Errorf("未知传输方式 %q", m.Transport) } diff --git a/server/platform_tools.go b/server/platform_tools.go index 1dacf56..aa7ab2d 100644 --- a/server/platform_tools.go +++ b/server/platform_tools.go @@ -254,6 +254,7 @@ type mcpToolInput struct { Env json.RawMessage `json:"env"` URL string `json:"url"` Enabled *bool `json:"enabled"` + Insecure *bool `json:"insecure"` } func mcpSchema(withID bool) map[string]any { @@ -265,6 +266,7 @@ func mcpSchema(withID bool) map[string]any { "env": map[string]any{"type": "object", "description": "环境变量 {KEY:VALUE}"}, "url": strParam("http/sse 的 URL"), "enabled": map[string]any{"type": "boolean", "description": "是否启用(默认 true)"}, + "insecure": map[string]any{"type": "boolean", "description": "http: 跳过 TLS 证书校验(自签证书时置 true, 默认 false)"}, } required := []string{"name", "transport"} if withID { @@ -279,9 +281,13 @@ func (a mcpToolInput) toDB() *db.MCPServer { if a.Enabled != nil { enabled = *a.Enabled } + insecure := false + if a.Insecure != nil { + insecure = *a.Insecure + } return &db.MCPServer{ ID: a.ID, Name: a.Name, Transport: a.Transport, Command: a.Command, - Args: a.Args, Env: a.Env, URL: a.URL, Enabled: enabled, + Args: a.Args, Env: a.Env, URL: a.URL, Enabled: enabled, Insecure: insecure, } } diff --git a/server/sync_scopesentry.go b/server/sync_scopesentry.go index 9c5c33f..3645ae1 100644 --- a/server/sync_scopesentry.go +++ b/server/sync_scopesentry.go @@ -55,7 +55,7 @@ func (s *Server) scopeSentryClient(ctx context.Context) (*mcphttp.Client, error) if m.URL == "" { return nil, fmt.Errorf("数据源 %s 未配置 URL,请先配置", scopeSentryMCPName) } - return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env)) + return mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure) } // envHasValue reports whether the env/header map has any non-empty value @@ -102,7 +102,7 @@ func (s *Server) syncSSStatus(w http.ResponseWriter, r *http.Request) { if configured && m.Enabled { ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second) defer cancel() - if cl, cerr := mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env)); cerr == nil { + if cl, cerr := mcphttp.New(ctx, m.Name, m.URL, jsonStrMap(m.Env), m.Insecure); cerr == nil { if _, terr := cl.Tools(ctx); terr == nil { resp["reachable"] = true } diff --git a/web/src/app/(main)/system/mcp/page.tsx b/web/src/app/(main)/system/mcp/page.tsx index 3ee5247..267f6b8 100644 --- a/web/src/app/(main)/system/mcp/page.tsx +++ b/web/src/app/(main)/system/mcp/page.tsx @@ -31,6 +31,7 @@ type FormState = { args: string; url: string; env: string; + insecure: boolean; }; const emptyForm: FormState = { name: "", @@ -39,6 +40,7 @@ const emptyForm: FormState = { args: "", url: "", env: "", + insecure: false, }; export default function MCPPage() { @@ -111,6 +113,7 @@ export default function MCPPage() { args: (s.args ?? []).join(" "), url: s.url ?? "", env: envToText(s.env), + insecure: s.insecure ?? false, }); setTab("config"); setOpen(true); @@ -151,12 +154,14 @@ export default function MCPPage() { command: "", args: [] as string[], env: parseEnv(form.env), // 远程模式下 env 即请求头 + insecure: form.insecure, } : { transport: "stdio" as const, command: form.command.trim(), args: form.args.trim() ? form.args.trim().split(/\s+/) : [], env: parseEnv(form.env), + insecure: false, }; await api.saveMcpServer({ ...(editing ? { id: editing.id } : {}), @@ -284,6 +289,13 @@ export default function MCPPage() { value={form.url} onChange={(e) => setF({ url: e.target.value })} /> + )}
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b5bab8a..d5cf7d1 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1070,6 +1070,7 @@ export interface MCPServer { env: Record; url?: string; enabled: boolean; + insecure?: boolean; // http: skip TLS cert verification (self-signed servers) tools?: string[]; // mcp_tools_cache (names only, for the count) }