`AuthdogVerifier` from `@authdog/mcp-sdk` verifies the bearer token an MCP client presents to your Node.js MCP server. It checks the token locally against your environment's JWKS, or calls `GET {baseUrl}/v1/userinfo`, and returns an `AuthInfo` (subject, scopes, expiry, claims). Source is [`node/`](https://github.com/authdog/mcp-sdk/tree/main/node) in [authdog/mcp-sdk](https://github.com/authdog/mcp-sdk). The package is ESM, uses `axios`, and declares `node >=16`.

This is the MCP token verifier. The name `@authdog/mcp-sdk` also belongs to a separate private runtime package inside the Authdog platform monorepo (trust store, AuthZEN, telemetry). That package is not on npm. See [MCP SDK](/docs/mcp-sdk) for how the two relate.

## Install

```bash
npm install @authdog/mcp-sdk
# or
deno add npm:@authdog/mcp-sdk
```

To use the MCP adapter (`@authdog/mcp-sdk/mcp`), also install the MCP TypeScript SDK, version 1.32 or newer. It is an optional peer dependency.

```bash
npm install @modelcontextprotocol/sdk
```

## Configure

```ts
import { AuthdogVerifier } from "@authdog/mcp-sdk"

const verifier = new AuthdogVerifier({
  baseUrl: "https://api.authdog.com",
  issuer: "https://api.authdog.com/oidc/<environmentId>",
  audience: "https://mcp.example.com",
})
```

| Option | Type | Default | Behavior |
| --- | --- | --- | --- |
| `baseUrl` | `string` | required | Authdog API base URL. Trailing slashes are stripped. A missing or blank value throws `TypeError`. |
| `timeout` | `number` | `10000` | Request timeout in milliseconds. A falsy value (including `0`) falls back to `10000`. |
| `issuer` | `string` | none | Environment OIDC issuer. Setting it turns on local JWT verification. |
| `audience` | `string` | none | The token's `aud` must contain it. Set it in production so a token minted for another resource is refused. |
| `mode` | `"auto" \| "jwt" \| "userinfo"` | `"auto"` | `auto` verifies JWT-shaped tokens locally when `issuer` is set and sends other tokens to userinfo. `jwt` is local only. `userinfo` always calls the API. |
| `jwksUri` | `string` | discovered | Skip discovery and use this JWKS URL. |
| `algorithms` | `string[]` | RS256, ES256, EdDSA | `none` and HMAC algorithms are never accepted. |
| `clockSkewSeconds` | `number` | `60` | Tolerance for `exp` and `nbf`. |

Each request sends `Content-Type: application/json` and a `User-Agent` of `authdog-mcp-node-sdk`.

## Verify a token

```ts
import { AuthdogVerifier, AuthenticationError, APIError } from "@authdog/mcp-sdk"

const authInfo = await verifier.verifyToken("the-access-token")
console.log(authInfo.subject, authInfo.scopes, authInfo.expiresAt)
```

`verifyToken(accessToken: string, signal?: AbortSignal): Promise<AuthInfo>` sends the token to the right verifier. Pass an `AbortSignal` to cancel the in-flight request, for example when the incoming MCP request is cancelled. An empty or whitespace-only token throws `AuthenticationError` before any request is made.

`AuthInfo`:

| Field | Type | Value |
| --- | --- | --- |
| `token` | `string` | The token you passed in |
| `subject` | `string` | The caller: a user id, or the machine subject Authdog creates for an agent or M2M client. |
| `username` | `string` | `user.userName` when verified through userinfo |
| `email` | `string \| null` | First `user.emails[].value`, or `null` |
| `scopes` | `string[]` | From the `scope` or `scp` claim when verified locally. `[]` through userinfo. |
| `expiresAt` | `Date \| null` | From the token's `exp` when verified locally. Through userinfo, now plus `session.remainingSeconds` when that is greater than 0. Otherwise `null`. |
| `user` | `UserInfoResponse["user"] \| null` | Full user object through userinfo. `null` when verified locally. |
| `claims` | `Record<string, unknown>` | Verified JWT claims, local verification only |

Set `issuer` for agent traffic. Local verification needs no API call per request and reads scopes from the token.

## Local JWT verification

With `issuer` set, the SDK fetches the issuer's OIDC discovery document and JWKS once, caches the keys, and checks signature, `iss`, `aud`, `exp` and `nbf`. A JWT that fails is rejected with `AuthenticationError` and is never retried against userinfo. If discovery or the JWKS cannot be reached and no key is cached, verification fails with `APIError` (fail closed).

| Option | Default | Behavior |
| --- | --- | --- |
| `jwksCacheSeconds` | `300` | How long keys are cached. A failed refresh keeps using the cached keys. |
| `jwksCooldownSeconds` | `30` | An unknown `kid` triggers at most one refresh per cooldown, so a rotated key is picked up quickly. |
| `maxRetries`, `retryBaseDelayMs` | `0`, `100` | Opt-in bounded, jittered retry for timeouts, 502, 503 and 504. A 401 is never retried. |
| `userinfoCacheSeconds` | `0` (off) | Cache successful userinfo results, never past the token's own expiry. |
| `introspection` | off | `{ endpoint?, clientId, clientSecret }`. RFC 7662 introspection for opaque tokens, using a confidential client of your environment. |

## Scopes, challenges, and resource metadata

```ts
import {
  requireScopes,
  challengeForError,
  buildProtectedResourceMetadata,
} from "@authdog/mcp-sdk"

const auth = await verifier.verifyToken(token)
requireScopes(auth, ["tools:call"]) // throws InsufficientScopeError

// On failure, answer with the right status and WWW-Authenticate header:
const { status, header } = challengeForError(
  error,
  "https://mcp.example.com/.well-known/oauth-protected-resource",
)

// Serve this at /.well-known/oauth-protected-resource:
const metadata = buildProtectedResourceMetadata({
  resource: "https://mcp.example.com",
  authorizationServers: ["https://api.authdog.com/oidc/<environmentId>"],
  scopesSupported: ["tools:call"],
})
```

| Failure | Status |
| --- | --- |
| Missing or rejected token | 401 with a `Bearer` challenge |
| Insufficient scope | 403 with `error="insufficient_scope"` |
| Verification could not be completed (`APIError`) | 503 with no `Bearer` challenge, because the client did nothing wrong |

`buildChallenge` builds the header on its own.

## Wire into an MCP server

The MCP TypeScript SDK protects HTTP transports with `requireBearerAuth`, which delegates to a token verifier. `createMcpTokenVerifier` adapts `AuthdogVerifier` to that interface:

```ts
import express from "express"
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"
import { AuthdogVerifier } from "@authdog/mcp-sdk"
import { createMcpTokenVerifier } from "@authdog/mcp-sdk/mcp"

const authdog = new AuthdogVerifier({
  baseUrl: "https://api.authdog.com",
  issuer: "https://api.authdog.com/oidc/<environmentId>",
  audience: "https://mcp.example.com",
})

const app = express()

app.use(
  "/mcp",
  requireBearerAuth({
    verifier: createMcpTokenVerifier(authdog, {
      resource: "https://mcp.example.com",
    }),
    requiredScopes: ["tools:call"],
    resourceMetadataUrl:
      "https://mcp.example.com/.well-known/oauth-protected-resource",
    expectedResource: "https://mcp.example.com",
  }),
)
```

The adapter translates errors into the MCP SDK's own types, so `requireBearerAuth` answers correctly: a rejected token is a 401 with a `WWW-Authenticate` challenge, an insufficient scope is a 403, and a verification that could not be completed is a 500, because the MCP middleware has no 503. `requireBearerAuth` also rejects tokens that carry no expiry.

## Errors

All three classes are exported and extend `Error`. `name` is set to the class name.

| Class | Thrown when |
| --- | --- |
| `AuthdogError` | Base class for the others |
| `AuthenticationError` | Empty or blank token, or HTTP 401 (`Unauthorized - invalid or expired token`) |
| `APIError` | Timeout (`Request timed out: ...`), other non-2xx (`HTTP error <status>: <body>`), network failure (`Request failed: ...`), or a 2xx response with no `user` (`Invalid response: missing user in userinfo payload`) |

```ts
try {
  await verifier.verifyToken(token)
} catch (error) {
  if (error instanceof AuthenticationError) {
    // reject with 401
  } else if (error instanceof APIError) {
    // upstream or transport failure
  } else {
    throw error
  }
}
```

## Related

- [MCP SDK](/docs/mcp-sdk): language overview and the name collision
- [MCP authentication](/docs/mcp-authentication): how MCP clients obtain tokens
- [Python](/docs/mcp-sdk/python) and [Go](/docs/mcp-sdk/go) verifiers
