Files
summercms/modules/pier/prm.go
Jakub Zych 806e87c510 feat(19.7-02): Redis sessions, event fan-out, stateless protocol path and resource metadata
- 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
2026-10-22 18:23:43 +02:00

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)
})
}