From 8234bdf9f364e858ea627c7d1e33848205cd0e0c Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 8 Sep 2026 13:29:41 +0000 Subject: [PATCH] feat: add the mcp scope resource --- server/internal/services/scopes.go | 6 +++- server/internal/services/scopes_test.go | 43 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 server/internal/services/scopes_test.go diff --git a/server/internal/services/scopes.go b/server/internal/services/scopes.go index ce8a4f4..bd54131 100644 --- a/server/internal/services/scopes.go +++ b/server/internal/services/scopes.go @@ -11,7 +11,7 @@ import ( // the vocabulary below. var ErrInvalidScope = errors.New("invalid scope") -// ScopeResources is the whole vocabulary. Eight resources, each with :read and +// ScopeResources is the whole vocabulary. Ten resources, each with :read and // :write, and write implies read on the same resource. // // It is deliberately coarse. A scope per endpoint is a table nobody maintains, @@ -27,6 +27,10 @@ var ScopeResources = []string{ "workloads", "settings", "status", + // mcp:read is permission to reach the MCP endpoint at all; mcp:write is + // permission for its write tools, which are not merely refused without it + // but omitted from tools/list entirely. + "mcp", } const ( diff --git a/server/internal/services/scopes_test.go b/server/internal/services/scopes_test.go new file mode 100644 index 0000000..3d05ec9 --- /dev/null +++ b/server/internal/services/scopes_test.go @@ -0,0 +1,43 @@ +package services + +import "testing" + +// The MCP endpoint is reached with an ordinary scope from the ordinary +// vocabulary. A bespoke action verb here would be the first exception in a +// table whose whole value is having none. +func TestMCPScopesExist(t *testing.T) { + if err := ValidScopes([]string{"mcp:read"}); err != nil { + t.Errorf("ValidScopes(mcp:read) = %v, want nil", err) + } + if err := ValidScopes([]string{"mcp:write"}); err != nil { + t.Errorf("ValidScopes(mcp:write) = %v, want nil", err) + } + if err := ValidScopes([]string{"mcp:use"}); err == nil { + t.Error("ValidScopes(mcp:use) = nil, want an error") + } +} + +// Write implies read on the same resource, so a token minted with mcp:write +// alone still reaches the endpoint. +func TestMCPWriteImpliesRead(t *testing.T) { + if !ScopeSatisfied([]string{"mcp:write"}, "mcp:read") { + t.Error("mcp:write does not satisfy mcp:read") + } + if ScopeSatisfied([]string{"mcp:read"}, "mcp:write") { + t.Error("mcp:read satisfies mcp:write, want false") + } +} + +func TestAllScopesAdvertisesMCP(t *testing.T) { + want := map[string]bool{"mcp:read": false, "mcp:write": false} + for _, s := range AllScopes() { + if _, ok := want[s]; ok { + want[s] = true + } + } + for s, found := range want { + if !found { + t.Errorf("AllScopes() is missing %q", s) + } + } +}