- store_redis.go: records with PX TTLs, per-binding sets, pub/sub events, opened lazily from redis.url under cooler's prefix (nothing dials at mount) - events.go: pact.MCPSubjectChanged republished through the store; every instance recomputes the subject's sessions and stateless servers (list_changed) or ends them on revocation; the link cache is dropped - stateless.go: the SDK's stateless handler over per-identity servers for 2026-07-28 clients, routed by the protocol header or server/discover - prm.go: RFC 9728 metadata at both well-known paths; long GET streams re-verify their bearer every minute - TestPierSessionsSmoke, TestPierFanOutSmoke, TestPierStatelessSmoke on miniredis; README features, configuration and operational notes
60 lines
2.1 KiB
Go
60 lines
2.1 KiB
Go
package pier
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"github.com/modelcontextprotocol/go-sdk/auth"
|
|
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
|
)
|
|
|
|
// PRMScopes is the scopes_supported list of the protected resource
|
|
// metadata: the OAuth server's MCP scopes plus backend (D-21).
|
|
var PRMScopes = []string{"read", "write", "ai", "offline_access", "backend"}
|
|
|
|
// mountPRM serves the RFC 9728 protected resource metadata at
|
|
// /.well-known/oauth-protected-resource and at
|
|
// /.well-known/oauth-protected-resource<mcp.path>, through the host's
|
|
// plain GET route kind. The authorization
|
|
// server is the published verifier's resource origin when a verifier is
|
|
// published, else app.url. A mcp.resource that differs from the verifier's
|
|
// resource is logged as a warning at boot (Pitfall 8).
|
|
func (p *Pier) mountPRM() error {
|
|
issuer := p.cfg.AppURL
|
|
if verifier, ok := p.app.Lookup[pact.MCPTokenVerifier](); ok && verifier != nil {
|
|
if res := verifier.MCPResource(); res != "" {
|
|
if u, err := url.Parse(res); err == nil && u.Scheme != "" && u.Host != "" {
|
|
issuer = u.Scheme + "://" + u.Host
|
|
}
|
|
if res != p.cfg.Resource {
|
|
logger().Warn("pier: mcp.resource differs from the verifier's resource; clients that send the verifier's resource will be refused",
|
|
"mcp_resource", p.cfg.Resource, "verifier_resource", res)
|
|
}
|
|
}
|
|
}
|
|
meta := &oauthex.ProtectedResourceMetadata{
|
|
Resource: p.cfg.Resource,
|
|
ScopesSupported: append([]string(nil), PRMScopes...),
|
|
BearerMethodsSupported: []string{"header"},
|
|
}
|
|
if issuer != "" {
|
|
meta.AuthorizationServers = []string{issuer}
|
|
}
|
|
h := nosniff(auth.ProtectedResourceMetadataHandler(meta))
|
|
for _, path := range []string{wellKnownPRM, wellKnownPRM + p.cfg.Path} {
|
|
if err := p.host.MountJSON(http.MethodGet, path, h); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nosniff adds X-Content-Type-Options to a JSON document route.
|
|
func nosniff(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|