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
This commit is contained in:
cwz_eikoh
2026-03-19 15:04:57 +08:00
committed by GitHub
parent e51be81c7e
commit e701abe273
4 changed files with 77 additions and 11 deletions

View File

@@ -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>",
},
},
},

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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-Tokentoken、AK/SKCursor 双 header、X-API-KeyClaude 单 headertoken 或 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)
return ctx
}
// 将 userCred 放入 context
} else {
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")
return ctx
}
}
}
// 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
}