Toolbox version
us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:1.2.0 (also reproduces on main as of today)
Summary
Combining mcpEnabled: true on an authService with tool-level authRequired referencing that same auth service makes every authenticated tool invocation fail with HTTP 401 unauthorized Tool call: Please make sure you specify correct auth headers — even when the Bearer token is valid and the server-level MCP auth check at internal/server/server.go:514 passed.
Root cause
The MCP method router does a second, per-tool authorization pass that requires some auth service to have produced claims via GetClaimsFromHeader:
internal/server/mcp/v20251125/method.go:176-198:
for _, aS := range authServices {
claims, err := aS.GetClaimsFromHeader(ctx, header)
if err != nil { logger.DebugContext(ctx, err.Error()); continue }
if claims == nil { continue }
claimsFromAuth[aS.GetName()] = claims
}
// Tool authorization check
verifiedAuthServices := make([]string, len(claimsFromAuth))
...
isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized {
err = util.NewClientServerError(
"unauthorized Tool call: Please make sure you specify correct auth headers",
http.StatusUnauthorized, nil)
...
}
But the generic auth service's GetClaimsFromHeader short-circuits to nil, nil whenever mcpEnabled is true (internal/auth/generic/generic.go:186-189):
func (a AuthService) GetClaimsFromHeader(ctx context.Context, h http.Header) (map[string]any, error) {
if a.McpEnabled {
return nil, nil
}
...
}
So claimsFromAuth is always empty when MCP Auth is enabled, verifiedAuthServices is empty, and any tool with authRequired: [<name>] referencing that auth service fails tool.Authorized([]).
Reproduction
Server config:
kind: source
name: bq
type: bigquery
project: my-project
---
kind: authService
name: my-google-auth
type: generic
audience: <client_id>.apps.googleusercontent.com
authorizationServer: https://accounts.google.com
introspectionEndpoint: https://www.googleapis.com/oauth2/v3/tokeninfo
introspectionMethod: GET
introspectionParamName: access_token
mcpEnabled: true
scopesRequired: [openid, email]
---
kind: tool
name: query_thing
type: bigquery-execute-sql
source: bq
description: ...
authRequired:
- my-google-auth # ← this is the trap
- Complete OAuth via any MCP client (claude.ai, Inspector).
- Call
query_thing with a valid Bearer access token.
- Server-level
ValidateMCPAuth passes (introspection validates aud, scope, exp).
- Method router returns 401
unauthorized Tool call: Please make sure you specify correct auth headers.
- Logs show no clue about why — no audience/scope/issuer error, just the generic message.
Remove authRequired from the tool and the same setup works. So the combination is the bug, not either field alone.
Why this is a footgun
- The error message blames the client ("specify correct auth headers"), but the client did everything right.
- The fix (removing
authRequired) makes the security posture look weaker to a casual reader, even though MCP Auth already gates the entire server, so tool-level authRequired was always redundant.
- There's no startup-time check or warning when both are set together — the config loads cleanly and only the runtime behavior reveals the issue.
Suggested fixes (any one)
- Treat MCP-authenticated requests as verified at the tool layer. When
mcpEnabled is set, populate claimsFromAuth[authServiceName] from the context that ValidateMCPAuth already stashed via util.WithAuthTokenClaims, instead of calling GetClaimsFromHeader again.
- Skip tool-level
authRequired enforcement when MCP Auth is enabled. The server-level check is the authoritative gate in that mode; the tool list of acceptable auth services is meaningless when there's only one (MCP-gated) entry point.
- Reject the combination at config-load time with a clear error: "tool 'X' has authRequired: [Y], but authService 'Y' has mcpEnabled: true — these are mutually exclusive. Remove authRequired from the tool; MCP Auth gates the entire server."
Option 3 is the cheapest (config validation only) and prevents footgun behavior without changing runtime semantics. Option 1 or 2 actually makes the combination usable.
Related
The Google IdP path also currently hits #3240 (introspection parser can't read Google's string-typed numeric fields). Both bugs need fixing for the documented "Google as OIDC provider with MCP Auth" config to work end-to-end. Both reproduce against :1.2.0 and main.
Toolbox version
us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:1.2.0(also reproduces onmainas of today)Summary
Combining
mcpEnabled: trueon anauthServicewith tool-levelauthRequiredreferencing that same auth service makes every authenticated tool invocation fail with HTTP 401unauthorized Tool call: Please make sure you specify correct auth headers— even when the Bearer token is valid and the server-level MCP auth check atinternal/server/server.go:514passed.Root cause
The MCP method router does a second, per-tool authorization pass that requires some auth service to have produced claims via
GetClaimsFromHeader:internal/server/mcp/v20251125/method.go:176-198:But the
genericauth service'sGetClaimsFromHeadershort-circuits tonil, nilwhenevermcpEnabledis true (internal/auth/generic/generic.go:186-189):So
claimsFromAuthis always empty when MCP Auth is enabled,verifiedAuthServicesis empty, and any tool withauthRequired: [<name>]referencing that auth service failstool.Authorized([]).Reproduction
Server config:
query_thingwith a valid Bearer access token.ValidateMCPAuthpasses (introspection validates aud, scope, exp).unauthorized Tool call: Please make sure you specify correct auth headers.Remove
authRequiredfrom the tool and the same setup works. So the combination is the bug, not either field alone.Why this is a footgun
authRequired) makes the security posture look weaker to a casual reader, even though MCP Auth already gates the entire server, so tool-levelauthRequiredwas always redundant.Suggested fixes (any one)
mcpEnabledis set, populateclaimsFromAuth[authServiceName]from the context thatValidateMCPAuthalready stashed viautil.WithAuthTokenClaims, instead of callingGetClaimsFromHeaderagain.authRequiredenforcement when MCP Auth is enabled. The server-level check is the authoritative gate in that mode; the tool list of acceptable auth services is meaningless when there's only one (MCP-gated) entry point.Option 3 is the cheapest (config validation only) and prevents footgun behavior without changing runtime semantics. Option 1 or 2 actually makes the combination usable.
Related
The Google IdP path also currently hits #3240 (introspection parser can't read Google's string-typed numeric fields). Both bugs need fixing for the documented "Google as OIDC provider with MCP Auth" config to work end-to-end. Both reproduce against
:1.2.0andmain.