Files
summercms/modules/pier/prm.go
Jakub Zych 63d05e8acb feat(32.1-01): publish one MCP scope list and call context
- pact gains MemberTokenScopeCatalog, MCPAdvertisedScopes and MCPCall
- an empty MCP ScopesSupported resolves to the five fixed names including backend
- pier copies advertised scopes and sets MCPCall beside WithDispatch
2026-10-28 14:32:31 +01:00

67 lines
2.4 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 when the published verifier does not advertise a non-empty
// list: the fixed MCP scopes, including backend.
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
scopes := append([]string(nil), PRMScopes...)
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)
}
}
if advertised, ok := verifier.(pact.MCPAdvertisedScopes); ok {
if list := advertised.MCPScopesSupported(); len(list) > 0 {
scopes = append([]string(nil), list...)
}
}
}
meta := &oauthex.ProtectedResourceMetadata{
Resource: p.cfg.Resource,
ScopesSupported: scopes,
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)
})
}