An MCP server exposes token_verifier. Implement that hook so a missing or rejected Authdog bearer never reaches a tool. The sample's whoami tool returns the subject the verifier already stored.
Sample: mcp/python. Python 3.10+. Package mcp==2.3.0.
MCP authentication is the hosted login an MCP client uses to obtain a token. This page is the resource-server check after the client presents one. The sample's caller is an agent client_credentials token, the same mint as A2A.
The verifier
Parse PK_AUTHDOG with validate_and_parse_public_key. That payload carries identity_host and environment_id. verify_token calls fetch_user_data — the same userinfo check as require_auth — and returns an MCP AccessToken only when the payload is authenticated and has a subject.
class AuthdogTokenVerifier:
async def verify_token(self, token: str) -> AccessToken | None:
try:
data = await fetch_user_data(
self._payload.identity_host,
self._payload.environment_id,
token,
)
except httpx.HTTPError:
return None
if not is_authenticated_user_info(data):
return None
subject = subject_of(data.get("user"))
if not subject:
return None
return AccessToken(
token=token,
client_id=subject,
scopes=["agent:invoke"],
subject=subject,
)None refuses the bearer. subject_of reads id, then sub.
Pass the verifier on MCPServer and point AuthSettings.issuer_url at {identity_host}/oidc/{environment_id}:
server = MCPServer(
name="authdog-mcp",
token_verifier=AuthdogTokenVerifier(payload),
auth=AuthSettings(
issuer_url=issuer,
resource_server_url=resource_url,
required_scopes=["agent:invoke"],
validate_token_resource=False,
),
)validate_token_resource=False because this verifier checks the token by userinfo. It does not require an aud claim equal to the MCP resource URL. Set the audience on the Authdog client when you need that binding. The sample does not add a second JWT parser.
The tool
@server.tool()
def whoami() -> str:
"""Return the Authdog subject of the calling agent."""
token = get_access_token()
if token is None or not token.subject:
raise PermissionError("Unauthorized")
return token.subjectget_access_token() is the MCP auth context the verifier filled in. The tool reads subject from that context.
What you should see
POST /mcp with no bearer returns 401. The initialize method does not run as an authenticated session.
Repeat with Authorization: Bearer <token> that userinfo accepts, then call whoami. The text is the Authdog subject.
pytest in the sample checks the unauthenticated 401 and that the verifier returns subject agent_1 for an accepted userinfo response.
Configure
| Variable | Purpose |
|---|---|
PK_AUTHDOG |
Public key. The server never sees the client secret. |
MCP_RESOURCE_URL |
Resource server URL passed to AuthSettings. Defaults to http://127.0.0.1:8000. |
Related
| Read | To learn how to |
|---|---|
| MCP authentication | OAuth 2.1 metadata and the client login flow |
| MCP SDK | AuthdogVerifier and the runtime guard |
| LangGraph | The same userinfo gate in front of a graph |