feat: serve the mcp endpoint behind the licence feature

This commit is contained in:
2026-09-08 13:57:21 +00:00
parent 674236bb76
commit 0166b17299
7 changed files with 157 additions and 0 deletions
+10
View File
@@ -8,8 +8,10 @@ import (
"strconv"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/mcp"
"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"
)
@@ -120,6 +122,14 @@ func RegisterRoutes(r *gin.Engine) {
apiGroup.POST("/console/connect", RequireFeature("console"), consoleConnect)
apiGroup.GET("/console/tunnel", RequireFeature("console"), consoleTunnel)
// MCP is mounted inside /api so that bearer auth, rate limiting, licence
// activity and RequireScopes all apply from where it lives rather than
// because someone remembered. The route-level scope is a floor: one route
// serves many tools, so per-tool scopes are enforced inside the handler.
mcpGroup := apiGroup.Group("/mcp", RequireFeature(license.FeatureMCP))
mcpGroup.POST("", mcp.Handler())
mcpGroup.GET("", mcp.Handler())
registerWorkflowRoutes(apiGroup)
registerMonitorRoutes(apiGroup)
registerChannelRoutes(apiGroup)
+6
View File
@@ -43,6 +43,12 @@ var routeScopes = map[string]string{
"GET /api/agent/latest-version": "servers:read",
"GET /api/audit": "settings:read",
// Both MCP routes require mcp:read as a floor. Individual tools require
// their own resource scope, and write tools additionally require mcp:write,
// enforced inside the handler because one route serves many operations.
"POST /api/mcp": "mcp:read",
"GET /api/mcp": "mcp:read",
"GET /api/settings": "settings:read",
"PUT /api/settings": "settings:write",
"POST /api/settings/secrets-token": "settings:write",
+12
View File
@@ -66,6 +66,18 @@ var serverScopedRoutes = map[string]bool{
"GET /api/servers/tags": false,
}
// POST/GET /api/mcp is deliberately absent from this map. main.go's
// serverTouchingRoutes only feeds in routes whose path contains "server" or
// "console" (or the one named workflow-run exception), and /api/mcp matches
// none of those, so it is never presented to AssertServerScopeMapComplete —
// there is nothing to declare true or false here. That is the right outcome:
// the single MCP route fronts many tools of very different shapes, several of
// which touch no server data at all, so a route-level entry could not say
// anything meaningful about tag scoping. Each tool that does read or act on
// server data applies auth.ServerScope itself, the same selector the REST
// handlers for those resources already apply, which is where this kind of
// scoping decision belongs for a many-operations-per-route endpoint.
// AssertServerScopeMapComplete refuses to boot when a route touching server
// data is missing from serverScopedRoutes.
func AssertServerScopeMapComplete(routes []string) error {