mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-21 05:57:53 +00:00
feat: MCP/A2A registry and authed streaming proxy
Add GET /v1/mcp/servers and GET /v1/agents: group-filtered,
name-sorted registry listings of configured MCP servers and A2A
agents, with their gateway-relative proxy (and, for agents, card)
URLs.
Add handleTargetProxy for /mcp/{name}/{rest...} and
/a2a/{name}/{rest...}: reverse-proxies to the target's configured
URL. Checks run unknown name (404), group deny via allowsMCP/
allowsAgent (403), request-rate limits (429/503), then Upgrade
(501) and path traversal (400) around the proxy call itself. No
upstream credential is injected — MCP servers and A2A agents are
in-cluster targets, unlike native provider passthrough.
Extract proxyUpstream from routes_passthrough.go's handlePassthrough
into a shared reverse-proxy core (header stripping, SSE-safe
flushWriter streaming, dead-upstream 502, canceled-client log-only,
optional JSON usage teeing) used by both native passthrough and the
new target proxy. Behavior-preserving for Task 13: its tests pass
unmodified.
Validate every configured MCP-server and agent target URL at
construction (url.Parse, http/https scheme only) — a malformed or
non-HTTP target is a constructor error, not a per-request 502.
This commit is contained in:
+43
-1
@@ -107,7 +107,12 @@ type Gateway struct {
|
||||
limiter *limiter
|
||||
registry *modelRegistry
|
||||
adapters map[string]providerAdapter
|
||||
name string
|
||||
// targetClient is the shared, connection-pooled *http.Client the
|
||||
// MCP/A2A target proxy (mcp_a2a.go) issues every upstream request
|
||||
// through — built once via newAdapterHTTPClient, the same constructor
|
||||
// each provider adapter uses for its own client.
|
||||
targetClient *http.Client
|
||||
name string
|
||||
}
|
||||
|
||||
// New creates the middleware. NOTE: no tail call — Yaegi zeroes
|
||||
@@ -124,6 +129,9 @@ func newGateway(ctx context.Context, next http.Handler, config *Config, name str
|
||||
if config == nil || len(config.Providers) == 0 {
|
||||
return nil, errors.New("llmgateway: at least one provider must be configured")
|
||||
}
|
||||
if err := validateTargetURLs(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth, err := newAuthStore(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -148,6 +156,7 @@ func newGateway(ctx context.Context, next http.Handler, config *Config, name str
|
||||
return nil, err
|
||||
}
|
||||
g.adapters = adapters
|
||||
g.targetClient = newAdapterHTTPClient()
|
||||
|
||||
registry, err := newModelRegistry(adapters, config, g.errorf)
|
||||
if err != nil {
|
||||
@@ -268,6 +277,39 @@ func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet && (r.URL.Path == "/v1/mcp/servers" || r.URL.Path == "/v1/agents") {
|
||||
_, grp, ok := g.auth.identify(r)
|
||||
if !ok {
|
||||
writeOAIError(sw, http.StatusUnauthorized, "authentication_error", "invalid or missing API key")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/mcp/servers" {
|
||||
g.handleMCPServers(sw, grp)
|
||||
} else {
|
||||
g.handleAgents(sw, grp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if name, rest, ok := targetRoute(r.URL.EscapedPath(), targetKindMCP); ok {
|
||||
u, grp, authOK := g.auth.identify(r)
|
||||
if !authOK {
|
||||
writeOAIError(sw, http.StatusUnauthorized, "authentication_error", "invalid or missing API key")
|
||||
return
|
||||
}
|
||||
g.handleTargetProxy(sw, r, u, grp, targetKindMCP, name, rest)
|
||||
return
|
||||
}
|
||||
if name, rest, ok := targetRoute(r.URL.EscapedPath(), targetKindAgent); ok {
|
||||
u, grp, authOK := g.auth.identify(r)
|
||||
if !authOK {
|
||||
writeOAIError(sw, http.StatusUnauthorized, "authentication_error", "invalid or missing API key")
|
||||
return
|
||||
}
|
||||
g.handleTargetProxy(sw, r, u, grp, targetKindAgent, name, rest)
|
||||
return
|
||||
}
|
||||
|
||||
if providerName, rest, ok := passthroughRoute(r.URL.EscapedPath()); ok {
|
||||
if _, known := g.adapters[providerName]; known {
|
||||
u, grp, ok := g.auth.identify(r)
|
||||
|
||||
Reference in New Issue
Block a user