first commit
This commit is contained in:
227
examples/mcps/auth-demo-server/main.go
Normal file
227
examples/mcps/auth-demo-server/main.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package main
|
||||
|
||||
// auth-demo-server demonstrates two layers of authentication for HTTP MCP servers:
|
||||
//
|
||||
// 1. CONNECTION-LEVEL AUTH (X-API-Key header)
|
||||
// Enforced in HTTP middleware on every request (initialize, tools/list,
|
||||
// tools/call). A missing or wrong key is rejected before the MCP server
|
||||
// sees the message at all.
|
||||
//
|
||||
// 2. TOOL-EXECUTION AUTH (X-Tool-Token header)
|
||||
// A separate secret token checked exclusively inside sensitive tool handlers
|
||||
// at call time. Public tools ignore it; the connection middleware does not
|
||||
// inspect it at all. This lets you scope a second credential to tool
|
||||
// execution only — distinct from the connection credential.
|
||||
//
|
||||
// HOW BIFROST SENDS HEADERS
|
||||
//
|
||||
// Bifrost has a single `headers` field on MCPClientConfig. Those same headers are
|
||||
// used in two places:
|
||||
// - At connection time: passed to transport.WithHTTPHeaders() so every HTTP
|
||||
// request to the server carries them.
|
||||
// - At tool-call time: copied onto CallToolRequest.Header so the server can
|
||||
// read them inside the tool handler via the request context.
|
||||
//
|
||||
// This means all configured headers are present on EVERY request — there is no
|
||||
// separate "connection-only" vs "tool-only" header mechanism in Bifrost. To
|
||||
// distinguish the two auth levels you simply use different header names, both
|
||||
// configured in the same `headers` map. The server then enforces each header
|
||||
// at the appropriate layer (middleware vs. handler).
|
||||
//
|
||||
// Bifrost config example:
|
||||
//
|
||||
// {
|
||||
// "name": "auth_demo",
|
||||
// "connection_type": "http",
|
||||
// "connection_string": "http://localhost:3002/",
|
||||
// "auth_type": "headers",
|
||||
// "headers": {
|
||||
// "X-API-Key": "super-secret-key",
|
||||
// "X-Tool-Token": "tool-exec-secret"
|
||||
// },
|
||||
// "tools_to_execute": ["*"]
|
||||
// }
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// connectionAPIKey is checked in HTTP middleware on every request
|
||||
// (initialize, tools/list, tools/call).
|
||||
// In production, load this from an environment variable or secrets manager.
|
||||
connectionAPIKey = "super-secret-key"
|
||||
|
||||
// toolExecToken is checked exclusively inside sensitive tool handlers —
|
||||
// never in the connection middleware. It acts as a second independent
|
||||
// credential that gates tool execution only.
|
||||
// In production, load this from an environment variable or secrets manager.
|
||||
toolExecToken = "tool-exec-secret"
|
||||
)
|
||||
|
||||
// contextKey is a private type so we don't collide with other packages' context keys.
|
||||
type contextKey string
|
||||
|
||||
const requestHeadersKey contextKey = "request_headers"
|
||||
|
||||
func main() {
|
||||
s := server.NewMCPServer("auth-demo-server", "1.0.0")
|
||||
|
||||
// public_info only requires connection-level auth (X-API-Key).
|
||||
// Any authenticated client can call it without a tool execution token.
|
||||
publicTool := mcp.NewTool(
|
||||
"public_info",
|
||||
mcp.WithDescription("Returns non-sensitive public information. Requires connection auth (X-API-Key) only."),
|
||||
mcp.WithString("topic", mcp.Required(), mcp.Description("Topic to look up")),
|
||||
)
|
||||
s.AddTool(publicTool, publicInfoHandler)
|
||||
|
||||
// secret_data requires BOTH connection-level auth (X-API-Key) AND a
|
||||
// dedicated tool-execution token (X-Tool-Token) checked inside the handler.
|
||||
// In Bifrost both headers live in the same `headers` map and arrive on
|
||||
// every request, so the handler reads X-Tool-Token from context and
|
||||
// validates it independently of the connection credential.
|
||||
secretTool := mcp.NewTool(
|
||||
"secret_data",
|
||||
mcp.WithDescription("Returns sensitive data. Requires connection auth (X-API-Key) AND tool-execution auth (X-Tool-Token)."),
|
||||
mcp.WithString("resource", mcp.Required(), mcp.Description("Resource name to fetch")),
|
||||
)
|
||||
s.AddTool(secretTool, secretDataHandler)
|
||||
|
||||
httpServer := server.NewStreamableHTTPServer(s)
|
||||
|
||||
// Middleware chain (outermost = first to run):
|
||||
// 1. connectionAuthMiddleware — rejects requests with a wrong/missing X-API-Key
|
||||
// 2. injectHeadersMiddleware — stores the request headers in context so
|
||||
// tool handlers can read them for tool-level auth
|
||||
// 3. httpServer — the MCP server itself
|
||||
handler := connectionAuthMiddleware(injectHeadersMiddleware(httpServer))
|
||||
|
||||
addr := "localhost:3002"
|
||||
log.Printf("auth-demo-server listening on http://%s/", addr)
|
||||
log.Printf("\nAuth layers:")
|
||||
log.Printf(" Connection-level: X-API-Key: %s (middleware rejects all requests without it)", connectionAPIKey)
|
||||
log.Printf(" Tool-execution: X-Tool-Token: %s (only secret_data checks this, validated inside the handler)", toolExecToken)
|
||||
log.Printf("\nNote: Bifrost sends all `headers` on both connection setup AND every tool call.")
|
||||
log.Printf("Both X-API-Key and X-Tool-Token go in the same `headers` map.")
|
||||
log.Printf("The server enforces each at the right layer: middleware vs. handler.\n")
|
||||
log.Printf("Bifrost config:")
|
||||
log.Printf(`
|
||||
{
|
||||
"name": "auth_demo",
|
||||
"connection_type": "http",
|
||||
"connection_string": "http://%s/",
|
||||
"auth_type": "headers",
|
||||
"headers": {
|
||||
"X-API-Key": "%s",
|
||||
"X-Tool-Token": "%s"
|
||||
},
|
||||
"tools_to_execute": ["*"]
|
||||
}
|
||||
`, addr, connectionAPIKey, toolExecToken)
|
||||
|
||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Middleware ───────────────────────────────────────────────────────────────
|
||||
|
||||
// connectionAuthMiddleware enforces connection-level authentication.
|
||||
// Every HTTP request — including initialize, tools/list, and tools/call —
|
||||
// must carry the correct X-API-Key header. A missing or wrong key results
|
||||
// in HTTP 401 before the MCP server processes anything.
|
||||
func connectionAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
http.Error(w, "connection auth required: missing X-API-Key header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if key != connectionAPIKey {
|
||||
http.Error(w, "connection auth failed: invalid X-API-Key", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// injectHeadersMiddleware stores the raw HTTP request headers in the context
|
||||
// so that tool handlers can read them for tool-level auth checks.
|
||||
// This is needed because MCP tool handlers only receive (ctx, CallToolRequest)
|
||||
// — they don't have direct access to the http.Request.
|
||||
func injectHeadersMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), requestHeadersKey, r.Header)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Tool handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
// publicInfoHandler handles "public_info". Connection auth has already been
|
||||
// verified by middleware, so no further auth check is needed here.
|
||||
func publicInfoHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var args struct {
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
if err := parseArgs(req, &args); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf(
|
||||
"Public info about %q: this data is available to all authenticated clients.", args.Topic,
|
||||
)), nil
|
||||
}
|
||||
|
||||
// secretDataHandler handles "secret_data". Connection-level auth (X-API-Key)
|
||||
// has already been verified by middleware. Here we additionally check
|
||||
// X-Tool-Token — a separate secret dedicated to authorizing tool execution.
|
||||
// Bifrost sends it as part of the same `headers` map, so it arrives on every
|
||||
// request including this tool call; the middleware intentionally ignores it.
|
||||
func secretDataHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// ── Tool-execution token check ───────────────────────────────────────────
|
||||
headers, ok := ctx.Value(requestHeadersKey).(http.Header)
|
||||
if !ok {
|
||||
return mcp.NewToolResultError("tool auth error: request headers unavailable in context"), nil
|
||||
}
|
||||
token := headers.Get("X-Tool-Token")
|
||||
if token == "" {
|
||||
return mcp.NewToolResultError("tool auth required: missing X-Tool-Token header"), nil
|
||||
}
|
||||
if token != toolExecToken {
|
||||
return mcp.NewToolResultError("tool auth failed: invalid X-Tool-Token"), nil
|
||||
}
|
||||
// ── Auth passed, proceed ─────────────────────────────────────────────────
|
||||
|
||||
var args struct {
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
if err := parseArgs(req, &args); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf(
|
||||
"Secret data for resource %q: [classified content — X-API-Key + X-Tool-Token verified]", args.Resource,
|
||||
)), nil
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func parseArgs(req mcp.CallToolRequest, dst any) error {
|
||||
b, err := json.Marshal(req.Params.Arguments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal arguments: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(b, dst); err != nil {
|
||||
return fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user