mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-06 21:52:54 +08:00
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:
@@ -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>",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user