> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcp-use.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Provider

> Integrate a DCR-capable OAuth authorization server with an MCP server.

Use `oauthCustomProvider` when your authorization server supports Dynamic Client Registration (DCR) but mcp-use does not provide a built-in adapter. The MCP client registers upstream; your server publishes metadata, verifies access tokens, and maps verified claims into application identity.

## Required contract

Import `MCPServer` from `mcp-use` and the provider contract from `mcp-use/oauth`.

```typescript theme={null}
import { MCPServer } from "mcp-use";
import {
  createJwtVerifier,
  oauthCustomProvider,
  type OAuthAuthInfo,
  type OAuthMetadata,
} from "mcp-use/oauth";

const issuer = "https://auth.example.com";

const oauthMetadata = {
  issuer,
  authorization_endpoint: `${issuer}/oauth/authorize`,
  token_endpoint: `${issuer}/oauth/token`,
  registration_endpoint: `${issuer}/oauth/register`,
  response_types_supported: ["code"],
  grant_types_supported: ["authorization_code", "refresh_token"],
  code_challenge_methods_supported: ["S256"],
} satisfies OAuthMetadata;

type AppUser = {
  id: string;
  email?: string;
  organizationId?: string;
};

const server = new MCPServer({
  name: "custom-auth-server",
  version: "1.0.0",
  oauth: oauthCustomProvider<AppUser>({
    oauthMetadata,

    createTokenVerifier(resource) {
      return createJwtVerifier({
        issuer,
        jwksUrl: new URL(`${issuer}/.well-known/jwks.json`),
        resource,
      });
    },

    mapAuthInfo(authInfo: OAuthAuthInfo) {
      const payload = authInfo.extra?.payload as Record<string, unknown>;
      if (typeof payload.sub !== "string") {
        throw new Error("Verified token is missing sub");
      }

      return {
        user: {
          id: payload.sub,
          email: typeof payload.email === "string" ? payload.email : undefined,
          organizationId:
            typeof payload.organization_id === "string"
              ? payload.organization_id
              : undefined,
        },
        payload,
        permissions: Array.isArray(payload.permissions)
          ? payload.permissions.filter(
              (value): value is string => typeof value === "string",
            )
          : [],
      };
    },
  }),
});

export default server;
```

`createTokenVerifier(resource)` receives the resolved canonical MCP resource. The verifier must validate token signature or introspection, issuer, expiry, and that resource, then return SDK auth information including `scopes`, `expiresAt`, and the validated `resource`. `resource` must be a `URL` matching the MCP resource, `clientId` must be a string (`""` when the token has no client), and `expiresAt` must be in the future, in Unix seconds. Auth information that breaks this contract is refused like an invalid token.

If your authorization server issues standard JWTs, `createJwtVerifier` does all of that for you and is the same helper the built-in providers use. Given a `jwksUrl`, it checks the signature, issuer, required `exp` and `sub` claims, and resource binding. It then returns the auth information in the shape the SDK expects.

For standards-conformant JWT access tokens, the verifier checks the [`aud` claim](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3) for the MCP resource URL. The `aud` claim can be a string or an array containing the resource URL. [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html#section-2) defines `resource` as an authorization and token request parameter, not as a JWT access-token claim. For compatibility with providers that include a non-standard `resource` claim, the verifier accepts a string-valued `resource` claim equal to the MCP resource URL and uses it instead of `aud`. Pass `audience` when the provider's JWT audience differs from the MCP resource.

Write the verifier by hand when the provider does not issue JWTs, for example when you have to call a token introspection endpoint. Reject an invalid, expired, or revoked token by throwing `new OAuthError(OAuthErrorCode.InvalidToken, message)`, both exported from `mcp-use/oauth`. The client then gets the same sign-in challenge as for a missing token; see [How hosts see a refused call](/v2/typescript/server/authentication/mixed-auth#how-hosts-see-a-refused-call) for the format each host receives. An error that is not an `OAuthError`, such as a failed request to an unreachable introspection endpoint, is answered with `500`.

`oauthMetadata` is the authorization server's RFC 8414 metadata. It must describe the actual upstream endpoints and include the `registration_endpoint` used by MCP clients.

`mapAuthInfo(authInfo)` receives only verified SDK auth information. It must return:

* `user`: your provider-specific normalized user, with `id` as the stable subject;
* `payload`: the verified claims or introspection response;
* `permissions`: application permissions derived from verified data.

The server exposes those values as `ctx.auth.user`, `ctx.auth.payload`, and `ctx.auth.permissions`. `ctx.auth.scopes` remains the verifier's verified `authInfo.scopes`.

## Extend the server from the provider

Some authorization models need more than token verification: a consent step exposed as a tool, or a policy check before every tool call. Pass an optional `setup(host)` hook and the server invokes it once while mounting, before the first request is served.

```typescript theme={null}
import { oauthCustomProvider, type OAuthProviderHost } from "mcp-use/oauth";

const oauth = oauthCustomProvider<AppUser>({
  createTokenVerifier,
  oauthMetadata,
  mapAuthInfo,
  setup(host: OAuthProviderHost<AppUser>) {
    for (const tool of host.listTools()) {
      if (tool.annotations?.destructiveHint && !tool._meta?.["shop/tags"]) {
        throw new Error(`Destructive tool "${tool.name}" needs shop/tags`);
      }
    }

    host.use("mcp:tools/call", async (ctx, next) => {
      // Let the consent tool through, or consent could never be recorded.
      if (ctx.params.name === "grant_consent") return next();
      if (!(await isAllowed(ctx.auth, ctx.params.name))) {
        return {
          isError: true,
          content: [{ type: "text", text: "Complete consent first." }],
        };
      }
      return next();
    });

    host.tool(
      { name: "grant_consent", description: "Record consent." },
      async (_params, ctx) => recordConsent(ctx.auth.user),
    );

    host.instructions((current) =>
      `${current ?? ""}\n\nCall grant_consent before other tools.`.trim(),
    );
  },
});
```

The host exposes:

| Member | Purpose |
| - | - |
| `resourceUrl`, `basePath` | The resolved canonical resource URL and the MCP endpoint path. |
| `mixedAuth` | Whether the server accepts signed-out clients. Throw from `setup` if your model needs anonymous discovery and this is `false`. |
| `use(pattern, handler)` | Registers `mcp:` middleware, like `server.use()`. |
| `tool`, `resource`, `resourceTemplate`, `prompt` | Register provider-owned items, like the `server` methods of the same name. |
| `listTools()` | Returns every tool registered so far, so the provider can validate the application's tools. |
| `instructions(transform)` | Rewrites the instructions text advertised to clients. Each call receives the result of the previous one. |

Provider-owned items go through the same OAuth gate as application items, and `ctx.auth` carries the provider's user type. On a [mixed authentication](/v2/typescript/server/authentication/mixed-auth) server, a provider tool can declare `securitySchemes` exactly like an application tool: with `noauth`, signed-out clients can call it and `ctx.auth` is optional. Resources, resource templates, and prompts always require sign-in. `mcp:` middleware also runs on signed-out calls to `noauth` tools, so check `ctx.auth` before relying on it. Registering a name that the application already uses throws, so reserved names never silently replace application items.

`setup` must be synchronous, and the host only works while it runs; calling it later throws. If `setup` throws or returns a promise, the server does not start, and every later request fails with the same error.

## Verify the integration

Test that:

* the client discovers metadata containing the correct registration endpoint;
* client registration, PKCE authorization, and token exchange happen upstream;
* tokens with the wrong issuer, expiry, signature, or MCP resource are rejected;
* mapped identity and permissions contain only verified data.

## Next steps

<CardGroup cols={2}>
  <Card title="User Context" icon="user" href="/v2/typescript/server/authentication/user-context">
    Use the mapped user, scopes, and permissions in callbacks.
  </Card>

  <Card title="OAuth overview" icon="shield-check" href="/v2/typescript/server/authentication/index">
    Compare built-in providers.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.