From d95f299562471c1f1a17552707e574d9ada34bb5 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 9 Sep 2026 08:36:26 +0000 Subject: [PATCH] feat(tokens): refuse minting mcp scopes without the MCP licence feature --- server/internal/api/tokens.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/server/internal/api/tokens.go b/server/internal/api/tokens.go index 4c8fc6a..1bc080d 100644 --- a/server/internal/api/tokens.go +++ b/server/internal/api/tokens.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "net/http" + "strings" "gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth" "gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models" "gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services" + "gitea.hostxtra.co.uk/vantage/vantage-shared/license" "github.com/gin-gonic/gin" ) @@ -109,6 +111,30 @@ func createToken(c *gin.Context) { } } + // The MCP scopes are refused without the licence feature, matching the + // guard-at-source thinking in services/packages.go rather than relying on + // RequireFeature at the /api/mcp group alone. That gate is a runtime one: + // without this, a licence downgrade leaves live agent credentials that + // authenticate, list as MCP tokens in the UI, and then fail mid- + // conversation with a 403 the model cannot explain. Refusing at minting + // means a token carrying mcp:* only ever existed while the feature did. + // + // Existing tokens are deliberately untouched by a downgrade: the route + // gate already stops them reaching the endpoint, and silently revoking + // credentials on a billing change is worse than refusing new ones. + if !services.GetLicenseState(auth.InstanceID(c)).Feature(license.FeatureMCP) { + for _, s := range body.Scopes { + if strings.HasPrefix(s, "mcp:") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "feature_unavailable", + "feature": license.FeatureMCP, + "code": "feature_unavailable", + }) + return + } + } + } + if !services.SelectorNarrowerOrEqual(body.TagSelector, auth.ServerScope(c)) { c.JSON(http.StatusForbidden, gin.H{ "error": "a token cannot reach servers its creator cannot reach",