feat(mcp/v2): cover the full v1 MCPAuth surface in AuthSpecKind

The first cut declared only the five OAuth-grant-shaped modes, which silently dropped
two live v1 auth_types: none (no upstream credential, the default for public upstreams)
and aws_sigv4 (per-request SigV4 signing for Bedrock AgentCore-style upstreams). Neither
had a home, so a server configured with them would have fallen through the resolver.

Adds both as AuthSpecKind members with their resolve() arms (Phase 0 stubs), and records
that the static-header family v1 splits across bearer_token/api_key/basic/token/authorization
collapses into the single api_key mode with the scheme carried as a parameter rather than
its own mode. The match-exhaustiveness gate forced the spike's label_enum to grow the two
arms too, which is the gate doing its job.
This commit is contained in:
Tin Chi Lo 2026-06-17 14:01:15 -07:00
parent 3d2ef7472f
commit d121a72f30
3 changed files with 32 additions and 6 deletions

View file

@ -55,8 +55,11 @@ _spike_exhaustiveness.py:44:18 - error: Argument of type "Literal[AuthSpecKind.a
assigned to parameter "arg" of type "Never" in function "assert_never" (reportArgumentType)
```
Restore the arm and the errors disappear. This is why adding a sixth `AuthSpecKind` member without a
`resolve()` arm fails the type gate rather than failing at runtime.
Restore the arm and the errors disappear. This is why adding a new `AuthSpecKind` member without a
`resolve()` arm fails the type gate rather than failing at runtime. `AuthSpecKind` covers v1's full
`MCPAuth` surface: the three OAuth grants (`authorization_code`, `client_credentials`,
`token_exchange`), the collapsed static-header family (`api_key`), client `passthrough`, `none`
(no upstream auth), and `aws_sigv4` (per-request signing).
## Toolchain notes

View file

@ -41,6 +41,10 @@ def label_enum(kind: AuthSpecKind) -> str:
return "static header"
case AuthSpecKind.passthrough:
return "client-forwarded"
case AuthSpecKind.none:
return "no upstream auth"
case AuthSpecKind.aws_sigv4:
return "aws sigv4 signing"
# Reached only if an enum member has no arm above. basedpyright then narrows `kind` to that
# uncovered member (not `Never`), so this `assert_never` is a type error => the gate bit.
assert_never(kind)

View file

@ -37,9 +37,12 @@ from ..result import Error, Ok, Result
class AuthSpecKind(str, Enum):
"""The server's statically-declared upstream-auth mode — the single source of truth.
Canonical names follow the OAuth-grant vocabulary (see the Notion "resolve() Per-Mode
Behavior" page). BYOK is *not* a member: it is the `api_key` mode seeded per-user, a
source selector inside that arm.
Covers v1's full `MCPAuth` surface, not only OAuth grants: the three grant modes, the
collapsed static-header family, client passthrough, no-auth, and AWS request signing.
BYOK is *not* a member: it is the `api_key` mode seeded per-user, a source selector
inside that arm. The static-header schemes v1 splits into separate `MCPAuth` values
(`bearer_token`/`api_key`/`basic`/`token`/`authorization`) collapse into `api_key`; the
scheme is a parameter the arm carries, not its own mode.
"""
authorization_code = (
@ -47,8 +50,10 @@ class AuthSpecKind(str, Enum):
)
client_credentials = "client_credentials" # gateway service account (M2M)
token_exchange = "token_exchange" # RFC 8693 on-behalf-of
api_key = "api_key" # fixed/static header (BYOK = per-user-seeded source)
api_key = "api_key" # static header, any scheme (BYOK = per-user-seeded source)
passthrough = "passthrough" # client forwards an upstream-audience token
none = "none" # no upstream credential; resolve yields a no-op auth, never an error
aws_sigv4 = "aws_sigv4" # AWS SigV4 per-request signing (e.g. Bedrock AgentCore)
@tagged_union(frozen=True)
@ -163,6 +168,10 @@ class UpstreamCredentialProvider:
return self._api_key(subject, server)
case AuthSpecKind.passthrough:
return self._passthrough(subject, server)
case AuthSpecKind.none:
return self._none(subject, server)
case AuthSpecKind.aws_sigv4:
return self._aws_sigv4(subject, server)
# --- arms: Phase 0 stubs (errors-as-values, no raise). Filled in Phase 1. -------------
def _authorization_code(
@ -190,6 +199,16 @@ class UpstreamCredentialProvider:
) -> Result[httpx.Auth, CredError]:
return _todo(AuthSpecKind.passthrough)
def _none(
self, subject: Subject, server: ServerSpec
) -> Result[httpx.Auth, CredError]:
return _todo(AuthSpecKind.none)
def _aws_sigv4(
self, subject: Subject, server: ServerSpec
) -> Result[httpx.Auth, CredError]:
return _todo(AuthSpecKind.aws_sigv4)
def _todo(kind: AuthSpecKind) -> Result[httpx.Auth, CredError]:
return Error(