Skip to content

Commit b8aa072

Browse files
authored
Feature(mcp): support x-api-key authentication for mcp-server (#24471)
* feat(mcp-server): support base64 ak/sk * fix(mcp-agent): try to fix route of default-mcp-tools
1 parent bdc211a commit b8aa072

File tree

4 files changed

+77
-11
lines changed

4 files changed

+77
-11
lines changed

pkg/apigateway/handler/mcp_agent.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ func mcpServersConfigHandler(ctx context.Context, w http.ResponseWriter, r *http
3232
responseType := r.URL.Query().Get("type")
3333
switch responseType {
3434
case "claude":
35-
cmd := fmt.Sprintf("claude mcp add --transport sse %s --header \"X-API-Key: your-key-here\"", sseURL)
35+
// Claude 仅支持单个自定义 header,使用 X-API-Key。填写方式:
36+
// base64(ak:sk):`echo -n "你的AK:你的SK" | base64`,将输出填入
37+
cmd := fmt.Sprintf("claude mcp add --transport sse %s --header \"X-API-Key: <填写 token 或 base64(AK:SK)>\"", sseURL)
3638
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
3739
w.Write([]byte(cmd))
3840
return
@@ -42,13 +44,14 @@ func mcpServersConfigHandler(ctx context.Context, w http.ResponseWriter, r *http
4244
// default: return JSON (cursor format)
4345
}
4446

47+
// Cursor:在 headers 中填写控制台/CLI 获取的 Access Key 与 Secret Key
4548
config := map[string]interface{}{
4649
"mcpServers": map[string]interface{}{
4750
mcpServerOption.Options.MCPServerName: map[string]interface{}{
4851
"url": sseURL,
4952
"headers": map[string]string{
50-
"AK": "value",
51-
"SK": "value",
53+
"AK": "<填写 Access Key>",
54+
"SK": "<填写 Secret Key>",
5255
},
5356
},
5457
},

pkg/apigateway/handler/misc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ func (h *MiscHandler) Bind(app *appsrv.Application) {
111111
// mcp agent default chat stream (uses agent with default_agent=true)
112112
defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler))
113113
app.AddHandler3(defaultChatStream)
114-
// mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
115-
app.AddHandler(GET, prefix+"mcp_agents/default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
116114

117115
// syslog webservice handlers
118116
app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken)
@@ -122,6 +120,8 @@ func (h *MiscHandler) Bind(app *appsrv.Application) {
122120

123121
// mcp servers config
124122
app.AddHandler(GET, prefix+"mcp-servers-config", mcpServersConfigHandler)
123+
// mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
124+
app.AddHandler(GET, prefix+"default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
125125
}
126126

127127
func UploadHandlerInfo(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo {

pkg/mcp-server/adapters/cloudpods_adapter.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ import (
2626
"yunion.io/x/onecloud/pkg/mcp-server/options"
2727
)
2828

29+
// Context key 类型,用于从 HTTP Header 传入的 AK/SK 存入 context(供 Cursor/Claude 等客户端使用)
30+
type headerCredKey string
31+
32+
const (
33+
ContextKeyAK headerCredKey = "mcp_header_ak"
34+
ContextKeySK headerCredKey = "mcp_header_sk"
35+
)
36+
37+
// GetAKSKFromContext 从 context 中读取连接时通过 Header 传入的 AK/SK(未设置时返回空字符串)
38+
func GetAKSKFromContext(ctx context.Context) (ak, sk string) {
39+
if v := ctx.Value(ContextKeyAK); v != nil {
40+
if s, ok := v.(string); ok {
41+
ak = s
42+
}
43+
}
44+
if v := ctx.Value(ContextKeySK); v != nil {
45+
if s, ok := v.(string); ok {
46+
sk = s
47+
}
48+
}
49+
return ak, sk
50+
}
51+
2952
// CloudpodsAdapter 是与 Cloudpods API 交互的适配器,负责认证和资源管理
3053
type CloudpodsAdapter struct {
3154
client *mcclient.Client
@@ -68,6 +91,10 @@ func (a *CloudpodsAdapter) authenticate(ak string, sk string) (mcclient.TokenCre
6891
}
6992

7093
func (a *CloudpodsAdapter) getSession(ctx context.Context, ak string, sk string) (*mcclient.ClientSession, error) {
94+
// 若工具未传入 ak/sk,则使用连接时 Header 中的 AK/SK(与 Cursor/Claude 配置一致)
95+
if ak == "" && sk == "" {
96+
ak, sk = GetAKSKFromContext(ctx)
97+
}
7198
var userCred mcclient.TokenCredential
7299
if auth.IsAuthed() {
73100
userCred = policy.FetchUserCredential(ctx)

pkg/mcp-server/server/server.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ package server
1616

1717
import (
1818
"context"
19+
"encoding/base64"
1920
"fmt"
2021
"net/http"
22+
"strings"
2123

2224
"github.com/mark3labs/mcp-go/server"
2325

@@ -142,22 +144,56 @@ func (s *CloudpodsMCPServer) registerAllTools() error {
142144
// Start 以sse模式启动 mcp 服务
143145
func (s *CloudpodsMCPServer) Start() error {
144146
// 设置 contextFunc 来从 HTTP header 中提取认证信息并放入 context
147+
// 支持:X-Auth-Token(token)、AK/SK(Cursor 双 header)、X-API-Key(Claude 单 header:token 或 base64(ak:sk))
145148
contextFunc := func(ctx context.Context, r *http.Request) context.Context {
146149
tokenStr := r.Header.Get(api.AUTH_TOKEN_HEADER)
147-
if len(tokenStr) > 0 {
150+
akStr := r.Header.Get("AK")
151+
skStr := r.Header.Get("SK")
152+
apiKey := r.Header.Get("X-API-Key")
153+
154+
// 1) 优先使用 X-Auth-Token
155+
if tokenStr != "" {
148156
if auth.IsAuthed() {
149157
userCred, err := auth.Verify(ctx, tokenStr)
150158
if err != nil {
151159
log.Errorf("Verify token failed: %s", err)
160+
} else {
161+
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
162+
log.Debugf("UserCred set in context from HTTP header token")
152163
return ctx
153164
}
154-
// 将 userCred 放入 context
155-
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
156-
log.Debugf("UserCred set in context from HTTP header token")
157-
} else {
158-
log.Warningf("Auth manager not initialized, skipping token verification")
159165
}
160166
}
167+
168+
// 2) Cursor 方式:直接使用 AK、SK 两个 Header
169+
if akStr != "" && skStr != "" {
170+
ctx = context.WithValue(ctx, adapters.ContextKeyAK, akStr)
171+
ctx = context.WithValue(ctx, adapters.ContextKeySK, skStr)
172+
log.Debugf("AK/SK set in context from headers")
173+
return ctx
174+
}
175+
176+
// 3) Claude 方式:X-API-Key 可为 token,或 base64(ak:sk)
177+
if apiKey != "" {
178+
if auth.IsAuthed() {
179+
if userCred, err := auth.Verify(ctx, apiKey); err == nil {
180+
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_AUTH_TOKEN, userCred)
181+
log.Debugf("UserCred set in context from X-API-Key token")
182+
return ctx
183+
}
184+
}
185+
decoded, err := base64.StdEncoding.DecodeString(apiKey)
186+
if err == nil {
187+
parts := strings.SplitN(string(decoded), ":", 2)
188+
if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
189+
ctx = context.WithValue(ctx, adapters.ContextKeyAK, parts[0])
190+
ctx = context.WithValue(ctx, adapters.ContextKeySK, parts[1])
191+
log.Debugf("AK/SK set in context from X-API-Key base64(ak:sk)")
192+
return ctx
193+
}
194+
}
195+
}
196+
161197
return ctx
162198
}
163199

0 commit comments

Comments
 (0)