From e701abe27301eed763ce616db96ea25dc687b5ef Mon Sep 17 00:00:00 2001 From: cwz_eikoh Date: Thu, 19 Mar 2026 15:04:57 +0800 Subject: [PATCH] Automated cherry pick of #24471: Feature(mcp): support x-api-key authentication for mcp-server (#24501) * feat(mcp-server): support base64 ak/sk * fix(mcp-agent): try to fix route of default-mcp-tools --- pkg/apigateway/handler/mcp_agent.go | 9 ++-- pkg/apigateway/handler/misc.go | 4 +- pkg/mcp-server/adapters/cloudpods_adapter.go | 27 +++++++++++ pkg/mcp-server/server/server.go | 48 +++++++++++++++++--- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/pkg/apigateway/handler/mcp_agent.go b/pkg/apigateway/handler/mcp_agent.go index 6de9a19e95..310889540c 100644 --- a/pkg/apigateway/handler/mcp_agent.go +++ b/pkg/apigateway/handler/mcp_agent.go @@ -32,7 +32,9 @@ func mcpServersConfigHandler(ctx context.Context, w http.ResponseWriter, r *http responseType := r.URL.Query().Get("type") switch responseType { case "claude": - cmd := fmt.Sprintf("claude mcp add --transport sse %s --header \"X-API-Key: your-key-here\"", sseURL) + // Claude 仅支持单个自定义 header,使用 X-API-Key。填写方式: + // base64(ak:sk):`echo -n "你的AK:你的SK" | base64`,将输出填入 + cmd := fmt.Sprintf("claude mcp add --transport sse %s --header \"X-API-Key: <填写 token 或 base64(AK:SK)>\"", sseURL) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write([]byte(cmd)) return @@ -42,13 +44,14 @@ func mcpServersConfigHandler(ctx context.Context, w http.ResponseWriter, r *http // default: return JSON (cursor format) } + // Cursor:在 headers 中填写控制台/CLI 获取的 Access Key 与 Secret Key config := map[string]interface{}{ "mcpServers": map[string]interface{}{ mcpServerOption.Options.MCPServerName: map[string]interface{}{ "url": sseURL, "headers": map[string]string{ - "AK": "value", - "SK": "value", + "AK": "<填写 Access Key>", + "SK": "<填写 Secret Key>", }, }, }, diff --git a/pkg/apigateway/handler/misc.go b/pkg/apigateway/handler/misc.go index 9d73b800fc..ff3a832523 100644 --- a/pkg/apigateway/handler/misc.go +++ b/pkg/apigateway/handler/misc.go @@ -111,8 +111,6 @@ func (h *MiscHandler) Bind(app *appsrv.Application) { // mcp agent default chat stream (uses agent with default_agent=true) defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler)) app.AddHandler3(defaultChatStream) - // mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry) - app.AddHandler(GET, prefix+"mcp_agents/default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler)) // syslog webservice handlers app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken) @@ -122,6 +120,8 @@ func (h *MiscHandler) Bind(app *appsrv.Application) { // mcp servers config app.AddHandler(GET, prefix+"mcp-servers-config", mcpServersConfigHandler) + // mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry) + app.AddHandler(GET, prefix+"default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler)) } func UploadHandlerInfo(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo { diff --git a/pkg/mcp-server/adapters/cloudpods_adapter.go b/pkg/mcp-server/adapters/cloudpods_adapter.go index 2c63d41c84..576bfe92c9 100644 --- a/pkg/mcp-server/adapters/cloudpods_adapter.go +++ b/pkg/mcp-server/adapters/cloudpods_adapter.go @@ -26,6 +26,29 @@ import ( "yunion.io/x/onecloud/pkg/mcp-server/options" ) +// Context key 类型,用于从 HTTP Header 传入的 AK/SK 存入 context(供 Cursor/Claude 等客户端使用) +type headerCredKey string + +const ( + ContextKeyAK headerCredKey = "mcp_header_ak" + ContextKeySK headerCredKey = "mcp_header_sk" +) + +// GetAKSKFromContext 从 context 中读取连接时通过 Header 传入的 AK/SK(未设置时返回空字符串) +func GetAKSKFromContext(ctx context.Context) (ak, sk string) { + if v := ctx.Value(ContextKeyAK); v != nil { + if s, ok := v.(string); ok { + ak = s + } + } + if v := ctx.Value(ContextKeySK); v != nil { + if s, ok := v.(string); ok { + sk = s + } + } + return ak, sk +} + // CloudpodsAdapter 是与 Cloudpods API 交互的适配器,负责认证和资源管理 type CloudpodsAdapter struct { client *mcclient.Client @@ -68,6 +91,10 @@ func (a *CloudpodsAdapter) authenticate(ak string, sk string) (mcclient.TokenCre } func (a *CloudpodsAdapter) getSession(ctx context.Context, ak string, sk string) (*mcclient.ClientSession, error) { + // 若工具未传入 ak/sk,则使用连接时 Header 中的 AK/SK(与 Cursor/Claude 配置一致) + if ak == "" && sk == "" { + ak, sk = GetAKSKFromContext(ctx) + } var userCred mcclient.TokenCredential if auth.IsAuthed() { userCred = policy.FetchUserCredential(ctx) diff --git a/pkg/mcp-server/server/server.go b/pkg/mcp-server/server/server.go index 19d27b3b54..c046e987e9 100644 --- a/pkg/mcp-server/server/server.go +++ b/pkg/mcp-server/server/server.go @@ -16,8 +16,10 @@ package server import ( "context" + "encoding/base64" "fmt" "net/http" + "strings" "github.com/mark3labs/mcp-go/server" @@ -142,22 +144,56 @@ func (s *CloudpodsMCPServer) registerAllTools() error { // Start 以sse模式启动 mcp 服务 func (s *CloudpodsMCPServer) Start() error { // 设置 contextFunc 来从 HTTP header 中提取认证信息并放入 context + // 支持:X-Auth-Token(token)、AK/SK(Cursor 双 header)、X-API-Key(Claude 单 header:token 或 base64(ak:sk)) contextFunc := func(ctx context.Context, r *http.Request) context.Context { tokenStr := r.Header.Get(api.AUTH_TOKEN_HEADER) - if len(tokenStr) > 0 { + akStr := r.Header.Get("AK") + skStr := r.Header.Get("SK") + apiKey := r.Header.Get("X-API-Key") + + // 1) 优先使用 X-Auth-Token + if tokenStr != "" { if auth.IsAuthed() { userCred, err := auth.Verify(ctx, tokenStr) if err != nil { log.Errorf("Verify token failed: %s", err) + } else { + ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred) + log.Debugf("UserCred set in context from HTTP header token") return ctx } - // 将 userCred 放入 context - ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred) - log.Debugf("UserCred set in context from HTTP header token") - } else { - log.Warningf("Auth manager not initialized, skipping token verification") } } + + // 2) Cursor 方式:直接使用 AK、SK 两个 Header + if akStr != "" && skStr != "" { + ctx = context.WithValue(ctx, adapters.ContextKeyAK, akStr) + ctx = context.WithValue(ctx, adapters.ContextKeySK, skStr) + log.Debugf("AK/SK set in context from headers") + return ctx + } + + // 3) Claude 方式:X-API-Key 可为 token,或 base64(ak:sk) + if apiKey != "" { + if auth.IsAuthed() { + if userCred, err := auth.Verify(ctx, apiKey); err == nil { + ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred) + log.Debugf("UserCred set in context from X-API-Key token") + return ctx + } + } + decoded, err := base64.StdEncoding.DecodeString(apiKey) + if err == nil { + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + ctx = context.WithValue(ctx, adapters.ContextKeyAK, parts[0]) + ctx = context.WithValue(ctx, adapters.ContextKeySK, parts[1]) + log.Debugf("AK/SK set in context from X-API-Key base64(ak:sk)") + return ctx + } + } + } + return ctx }