Skip to content

fix(scope): OAuth users are scope-restricted like agent tokens on the direct surface - #1334

Merged
Dumbris merged 3 commits into
mainfrom
claude/eager-jackson-badeab
Sep 21, 2026
Merged

Dumbris merged 3 commits into
mainfrom
claude/eager-jackson-badeab

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 21, 2026

Copy link
Copy Markdown
Member

Summary

Continues Spec 105 scope hardening (PR G). Several direct-mode authorization gates decided "is this caller scope-restricted" with authCtx.Type == auth.AuthTypeAgent. A server-edition OAuth user context is not an administrator (IsAdmin() is false) but also isn't an agent token, so it fell through to the unrestricted branch — a user token scoped to one server could see and reach every configured server via:

  • tools/list on the direct surface (filterDirectModeToolsForAuth)
  • describe_tool's direct-surface id resolution (directEntryVisibleToSession)
  • prompts/list and prompts/get (filterAggregatedPromptsForAuth, promptServerAllowed)
  • the direct callability filters that hide pending/quarantined/disabled tools (filterDirectToolsForAgentCallability, directEntryCallable)

Fix

Replaced the Type-only check with isScopeRestrictedCaller(authCtx) = authCtx != nil && !authCtx.IsAdmin(), matching the existing auth.IsScopedCaller helper and cache_authz.go's own documented rule that "a User is caller-bounded exactly like an Agent." The listing (filterDirectToolsForAgentCallability) and describe-time (directEntryCallable) callability gates were fixed together to preserve the SC-007 listing/describe parity invariant, cross-checked against docs/development/server-edition-multiuser-auth.md (only the admin role is meant to be operator-visible into pending/quarantined tools).

Audited, left unchanged (verified not bugs)

  • cache_authz.go — already has its own correctly-scoped AuthTypeUser case
  • mcp.go's getAuthMetadata — copies agent-only fields into audit metadata, not an access decision
  • audit_funnel.go — a switch with an explicit case per auth type, not a binary gate
  • profile_resolver.go's profilePinFromContextProfilePin is a field only ever set on agent-token contexts (data-model fact, not an authz gate)
  • internal/httpapi/sse_scope.go — the real SSE scope gate (eventVisibleToCaller) already uses auth.IsScopedCaller; the flagged line only selects a token-refresh mechanism, and OAuth session principals are already routed correctly via an earlier branch

Test plan

  • New regression tests with AuthTypeUser fixtures (plus AdminUserContext positive controls) in mcp_direct_catalog_test.go, mcp_describe_direct_test.go, mcp_direct_callability_test.go, mcp_prompt_scope_test.go
  • go build ./... and go build -tags server ./...
  • go vet ./internal/server/... ./internal/httpapi/...
  • go test ./internal/httpapi/... ./internal/auth/...
  • go test -timeout 15m -skip "E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./internal/server/... (CI's skip pattern — bare runs hang on known binary-spawning tests, unrelated to this change)
  • go test -race on the full touched-surface test set (73 tests, no race flags)

🤖 Generated with Claude Code

… direct surface (Spec 105 PR G)

Several direct-mode gates keyed "is this caller scope-restricted" on
authCtx.Type == auth.AuthTypeAgent. A server-edition OAuth "user" context
is not an administrator (IsAdmin() is false) but also isn't an agent
token, so it fell through to the unrestricted branch: a user token scoped
to one server could see and reach every configured server through
tools/list, describe_tool, prompts/list+get, and the direct callability
filters.

Replaces the Type-only check with isScopeRestrictedCaller (authCtx != nil
&& !authCtx.IsAdmin()), consistent with the existing auth.IsScopedCaller
helper and with cache_authz.go's own "a User is caller-bounded exactly
like an Agent" rule. Fixed in mcp_direct_scope.go (filterDirectModeToolsForAuth,
filterAggregatedPromptsForAuth, promptServerAllowed), mcp_describe_direct.go
(directEntryVisibleToSession) and mcp_direct_callability.go
(filterDirectToolsForAgentCallability + directEntryCallable, kept in
parity per the SC-007 listing/describe invariant).

Audited but left unchanged (verified not bugs): cache_authz.go,
mcp.go's getAuthMetadata, audit_funnel.go, profile_resolver.go's
profilePinFromContext, and internal/httpapi/sse_scope.go — each either
already handles AuthTypeUser correctly or checks something other than
caller scope (a data-model fact or an audit-metadata field).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 21, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: b667ea4
Status: ✅  Deploy successful!
Preview URL: https://0bf967a4.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-eager-jackson-badeab.mcpproxy-docs.pages.dev

View logs

…aude/eager-jackson-badeab

# Conflicts:
#	internal/server/mcp_describe_direct.go
#	internal/server/mcp_direct_scope.go
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: claude/eager-jackson-badeab

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-goRLEE8B.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 35585824309 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

… time

codex gpt-5.6-sol cross-review (round 1) of PR #1334: the existing
TestDescribeDirect_ServerScopeGate_UserType scopes its OAuth user OUT of
the target server, so directEntryInScope rejects the tool before
directEntryVisibleToSession ever reaches directEntryCallable — it does
not exercise that function's own isScopeRestrictedCaller fix. Adds a
test that scopes the user INTO the server with a pending tool, proving
describe_tool refuses it via callability (with an approved sibling as
positive control). Verified non-vacuous: fails against the pre-fix
Type == AuthTypeAgent check, passes against isScopeRestrictedCaller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Dumbris
Dumbris merged commit 129d7bf into main Sep 21, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants