tower-mcp 0.17.2

Tower-native Model Context Protocol (MCP) implementation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#![doc = r####"
# OAuth authorization with tower-mcp

This guide explains how to assemble tower-mcp's OAuth resource-server and
client APIs. The [MCP 2026-07-28 authorization specification][mcp-auth] remains
the normative source for protocol behavior; this document focuses on library
choices, application policy, and runnable setup.

OAuth is optional in MCP and applies to HTTP-based transports. For stdio,
provide credentials through the process environment instead of running the MCP
HTTP authorization flow.

## Choose the application path

| You are building | tower-mcp entry point | Enable |
|---|---|---|
| Protected MCP HTTP server with local JWT keys | `HttpTransport::into_oauth_router` + `JwtValidator` | `oauth` |
| Protected MCP HTTP server with remote JWKS | `HttpTransport::into_oauth_router` + `JwksValidator` | `jwks` |
| Interactive desktop, CLI, or web client | `OAuthAuthorizationFlow` | `oauth-client` |
| Service-to-service client | `OAuthClientCredentials` | `oauth-client` |
| Existing identity or token system | `TokenValidator` or `TokenProvider` | `oauth` / `oauth-client` |

```toml
[dependencies]
tower-mcp = { version = "0.17", features = ["http", "jwks", "oauth-client"] }
```

The three OAuth roles stay separate:

- The MCP server is an OAuth **protected resource**. It publishes Protected
  Resource Metadata (PRM), validates access tokens, and enforces scopes.
- The MCP client is an OAuth **client**. It discovers metadata, obtains a client
  ID, drives authorization, stores tokens, and sends a bearer token on every
  MCP HTTP request.
- The authorization server authenticates the resource owner and issues tokens.
  tower-mcp deliberately does not implement an authorization server.

## Protect an MCP server

For production, prefer asymmetric access tokens and remote JWKS validation.
The canonical resource URI must be the externally visible identifier clients
use, including a path when that path distinguishes this MCP server.

```rust
use tower_mcp::{HttpTransport, McpRouter};
use tower_mcp::oauth::{
    JwksValidator, ProtectedResourceMetadata, ScopePolicy,
};

async fn protected_app(mcp: McpRouter) -> Result<axum::Router, tower_mcp::BoxError> {
let resource = "https://mcp.example.com/mcp";
let issuer = "https://login.example.com/tenant";

let validator = JwksValidator::builder(
        "https://login.example.com/tenant/.well-known/jwks.json",
    )
    .expected_issuer(issuer)
    .expected_audience(resource)
    .build()
    .await?;

let metadata = ProtectedResourceMetadata::new(resource)
    .authorization_server(issuer)
    .scope("mcp:read")
    .scope("mcp:write")
    .resource_documentation("https://docs.example.com/mcp/access");

// Tool-specific requirements are checked in addition to the default.
let scopes = ScopePolicy::new()
    .default_scope("mcp:read")
    .tool_scope("publish", "mcp:write")
    .resource_scope("secret://report", "reports:read");

let app = HttpTransport::new(mcp)
    .into_oauth_router_at("/mcp", validator, metadata, scopes)?;
Ok(app)
}
```

`into_oauth_router` and `into_oauth_router_at` are the safe composition paths.
They:

1. validate and serve path-aware [RFC 9728][rfc9728] metadata;
2. install `OAuthLayer` outside the HTTP transport to validate bearer tokens
   and the resource audience;
3. bridge validated `TokenClaims` into MCP request extensions; and
4. install fail-closed `ScopeEnforcementLayer` inside the transport for
   tool-, resource-, and prompt-specific policy.

Do not use `HttpTransport::oauth` alone for a protected endpoint. That
lower-level method publishes metadata but intentionally does not install token
or scope enforcement.

### Run the JWKS server example

The repository example uses the same composition and requires a real
authorization server that issues JWT access tokens:

```bash
export MCP_RESOURCE=http://127.0.0.1:3000
export OAUTH_ISSUER=https://login.example.com/tenant
export OAUTH_JWKS_URL=https://login.example.com/tenant/.well-known/jwks.json

cargo run --example http_auth --features jwks -- --auth jwks
```

Inspect the advertised metadata:

```bash
curl http://127.0.0.1:3000/.well-known/oauth-protected-resource
```

The example requires `mcp:read` for all operations and additionally requires
`mcp:write` for its `add` tool. Its `--auth oauth` mode uses a shared secret and
disables expiry validation for local experimentation only.

### JWT/JWKS validation policy

Configure both the validator and PRM from the same canonical resource and
issuer values. `OAuthLayer` independently checks that token `aud` contains the
PRM resource, while `expected_audience` and `expected_issuer` make the JWT
validator reject a token before it reaches MCP dispatch. Keep expiration
validation enabled.

`JwksValidator` caches keys, honors HTTP `Cache-Control: max-age`, uses a
five-minute fallback TTL, and refreshes after an unknown key ID with a minimum
refresh interval. Supply a custom `reqwest::Client` when the deployment needs
specific TLS roots, proxy behavior, or tighter timeouts.

`ScopePolicy` uses exact scope matching by default. If the authorization server
defines hierarchy, install an explicit matcher:

```rust
use tower_mcp::oauth::ScopePolicy;

let scopes = ScopePolicy::new()
    .scope_matcher(|granted: &str, required: &str| {
        granted == required || granted == "mcp:*"
    })
    .default_scope("mcp:read");
```

Keep this mapping authorization-server-specific. Do not infer hierarchy from
punctuation alone.

## Interactive authorization-code clients

`OAuthAuthorizationFlow` is a reusable state machine and token provider. It
implements discovery, client registration selection, PKCE S256, state and
callback validation, authorization-response issuer validation, resource
binding, token refresh, and runtime scope escalation.

For a native or CLI application, use a loopback redirect and let an
`OAuthAuthorizationHandler` present the URL to the user:

```rust
use async_trait::async_trait;
use tower_mcp::client::{
    HttpClientTransport, OAuthAuthorizationAction, OAuthAuthorizationFlow,
    OAuthAuthorizationHandler, OAuthAuthorizationRequest,
    OAuthClientRegistrationOptions, OAuthDynamicClientRegistration,
    OAuthRedirectPolicy, OAuthScopeEscalationConfig,
};

struct OpenBrowser;

#[async_trait]
impl OAuthAuthorizationHandler for OpenBrowser {
    async fn authorize(
        &self,
        request: OAuthAuthorizationRequest,
    ) -> Result<OAuthAuthorizationAction, tower_mcp::client::OAuthClientError> {
        // Open request.authorization_url with the platform browser here.
        println!("Authorize at: {}", request.authorization_url);
        Ok(OAuthAuthorizationAction::AwaitLoopback)
    }
}

async fn authorized_transport() -> Result<HttpClientTransport, tower_mcp::BoxError> {
let resource = "https://mcp.example.com/mcp";
let callback = "http://127.0.0.1:53682/oauth/callback";

let registration = OAuthClientRegistrationOptions::new()
    .with_client_id_metadata_document(
        "https://client.example.com/.well-known/mcp-oauth-client.json",
    )
    .with_dynamic_registration(
        OAuthDynamicClientRegistration::native("My MCP client", [callback])
            .grant_types(["authorization_code", "refresh_token"]),
    );

let flow = OAuthAuthorizationFlow::builder(resource)
    .registration_options(registration)
    .redirect_policy(OAuthRedirectPolicy::loopback_at(
        53682,
        "/oauth/callback",
    ))
    .authorization_handler(OpenBrowser)
    .build()?;

// An empty explicit set lets the flow prefer scopes from the initial
// WWW-Authenticate challenge, then PRM scopes_supported.
flow.authorize(std::iter::empty::<&str>()).await?;
let initial_scopes = flow.authorized_scopes().await.unwrap_or_default();

let transport = HttpClientTransport::new(resource)
    .with_scope_aware_token_provider(
        flow,
        OAuthScopeEscalationConfig::new(initial_scopes).max_attempts(2),
    );
Ok(transport)
}
```

The loopback listener is bound before the authorization URL is handed to the
application. The callback must contain the expected state, and the code is not
sent to the token endpoint until callback and issuer validation succeed. PKCE
uses S256; authorization servers that do not advertise it are rejected.

For a web application, use `OAuthRedirectPolicy::fixed`, store pending state in
a shared `OAuthAuthorizationStateStore`, and call
`OAuthPendingAuthorization::complete_callback_url` from the application's
callback route. Use `begin` instead of `authorize` when the UI needs explicit
`Authorized` versus `Pending` states.

### Run the interactive client example

The example uses a fixed loopback port. Configure that exact redirect URI in a
pre-registered client or Client ID Metadata Document:

```text
http://127.0.0.1:53682/oauth/callback
```

Then run one of these configurations:

```bash
# Pre-registered client (highest priority)
export MCP_SERVER_URL=https://mcp.example.com/mcp
export OAUTH_CLIENT_ID=my-client-id
export OAUTH_CLIENT_SECRET=my-client-secret # omit for a public client
cargo run --example oauth_client --features oauth-client -- \
  --mode authorization-code

# Client ID Metadata Document, with DCR retained as fallback
unset OAUTH_CLIENT_ID OAUTH_CLIENT_SECRET
export OAUTH_CLIENT_ID_METADATA_DOCUMENT=https://client.example.com/oauth/client.json
cargo run --example oauth_client --features oauth-client -- \
  --mode authorization-code
```

Leave `OAUTH_SCOPES` unset to follow challenge/PRM scope selection. Set it to a
space-separated list only when the application has an explicit operation plan.

### Registration policy

tower-mcp follows the final MCP selection priority:

1. pre-registered credentials when supplied;
2. a Client ID Metadata Document (CIMD) when configured and advertised;
3. Dynamic Client Registration (DCR) as a backwards-compatible fallback; then
4. an actionable error so the application can ask the user for credentials.

Use `pre_registered_client` when the client ID is known before discovery. Use
`OAuthClientRegistrationOptions` when configuring CIMD and DCR. DCR is
deprecated by the final MCP specification; new broadly distributed clients
should publish a CIMD, while enterprise deployments commonly use
pre-registration.

Pre-registered and dynamically registered credentials are bound to the exact
validated authorization-server issuer. `OAuthClientRegistrationStore`
implementations must key them by issuer and must never reuse them after PRM
selects a different authorization server. CIMD client IDs are portable HTTPS
URLs and are not issuer-bound.

When multiple authorization servers are advertised, set
`preferred_authorization_server` from trusted application/user policy. Without
it, the flow uses the first usable advertised server.

### Scope selection and step-up

With no explicit scopes, the flow uses this order:

1. `scope` from the initial `WWW-Authenticate` challenge;
2. `scopes_supported` from Protected Resource Metadata; then
3. authorization-server metadata as an interoperability fallback.

A non-empty scope list passed to `authorize` is an application override. Keep
it least-privileged. If the authorization server advertises both
`offline_access` and refresh-token support, the flow requests it and accepts
that a server may still decline to issue a refresh token.

`with_scope_aware_token_provider` handles a compliant runtime HTTP 403 Bearer
challenge with `error="insufficient_scope"`. It unions the challenged scopes
with the scopes already held, serializes concurrent reauthorization, and
retries the original operation up to the configured bound. The same
`OAuthAuthorizationFlow` must be installed as both token provider and
reauthorization handler so a new token becomes visible to the retry.

### Persistence and process topology

The default stores are in-memory and are suitable for examples and a
single-process desktop client. Production implementations normally replace all
three stores:

| Trait | Key/binding | Store securely |
|---|---|---|
| `OAuthClientRegistrationStore` | exact AS issuer | client ID, registration method, client secret |
| `OAuthTokenStore` | resource + issuer + client ID | access token, refresh token, scopes, expiry |
| `OAuthAuthorizationStateStore` | random state | PKCE verifier, redirect URI, issuer, resource, client registration |

Protect secrets at rest with the platform keychain, a secret manager, or
application-level authenticated encryption. Pending authorization state is
short-lived and single-use. A multi-instance web client needs shared,
atomic storage so the callback can land on a different instance without
reusing or losing state.

The `OAuthHttpClient` trait lets an application route all discovery,
registration, and token calls through its own HTTP stack. Redirects must remain
disabled: authorization endpoints are user-agent destinations, while metadata,
registration, and token endpoint redirects can cross trust boundaries.

## Service-to-service client credentials

For a client acting on its own behalf, use `OAuthClientCredentials`. Discovery
is preferred because it validates PRM/issuer metadata, binds the RFC 8707
resource, and selects a supported token-endpoint authentication method:

```rust
use tower_mcp::client::{HttpClientTransport, OAuthClientCredentials};

async fn service_transport() -> Result<HttpClientTransport, tower_mcp::BoxError> {
let provider = OAuthClientCredentials::discover(
    "https://mcp.example.com/mcp",
    "inventory-worker",
    "secret-from-vault",
)
.await?;

let transport = HttpClientTransport::new("https://mcp.example.com/mcp")
    .with_token_provider(provider);
Ok(transport)
}
```

The provider supports `client_secret_basic`, `client_secret_post`, and `none`
when selected by discovery or direct builder configuration. It caches access
tokens in memory and obtains a new client-credentials token before expiry;
client-credentials grants do not use refresh tokens. `private_key_jwt` is
supported by `OAuthAuthorizationFlow` when an `OAuthClientAssertionSigner` is
installed, not by `OAuthClientCredentials`.

Clients acting on their own behalf may abort rather than prompting on an
insufficient-scope response. The basic provider does not implement interactive
step-up; request the service's complete least-privileged scope set up front.

## Integrate a custom identity provider

tower-mcp keeps the application-specific seams explicit:

- Implement `TokenValidator` on the server for opaque-token introspection,
  session lookup, or a vendor SDK. Return normalized `TokenClaims`; the outer
  OAuth layer still performs resource-audience enforcement.
- Implement `TokenProvider` on a client when another component owns token
  acquisition or a workload identity system rotates tokens.
- Implement `OAuthAuthorizationHandler` to open a browser, display a device UI,
  or hand authorization to an application shell.
- Implement the three persistence traits to use a database or platform
  keychain, and `OAuthHttpClient` to apply corporate TLS/proxy policy.
- Implement `OAuthClientAssertionSigner` for `private_key_jwt`. Keep the private
  key in an HSM or secret service; the callback receives the exact issuer,
  token endpoint, client ID, audience, and assertion lifetime to sign.

Do not use a custom provider to skip PRM discovery, issuer binding, resource
indicators, callback validation, or audience checks. Those are protocol
security boundaries rather than identity-provider details.

## Production checklist

- Serve resource, metadata, authorization, token, registration, and CIMD HTTPS
  endpoints over authenticated TLS. Loopback redirects are the native-client
  exception described by [RFC 8252][rfc8252].
- Set PRM `resource` to the canonical public URI, not an internal service name;
  keep reverse-proxy path rewriting consistent with that identifier.
- Validate token signature, algorithm, issuer, audience, expiry, and any
  application-required claims. Never disable expiry validation in production.
- Use `into_oauth_router`/`into_oauth_router_at` and keep its middleware order.
  Preserve Host and Origin validation; configure explicit allowed values
  instead of calling `disable_origin_validation` outside local development.
- Keep PRM public, but do not expose broad path prefixes as unauthenticated.
- Require PKCE S256 and exact registered redirects. Keep `state`, PKCE verifier,
  authorization code, client secret, access token, and refresh token out of
  logs and error telemetry.
- Compare authorization-server issuer identifiers as exact strings. Do not
  normalize a trailing slash, case, port, or percent encoding before issuer
  comparison.
- Send the canonical `resource` in authorization and token requests and accept
  tokens only for that audience. Never pass through a token issued for an
  upstream service.
- Encrypt persistent credentials/tokens, use atomic single-use callback state,
  and partition all records by resource, issuer, and client.
- Configure a bounded scope-escalation retry count and require explicit user
  interaction when the application is acting on behalf of a person.
- Treat DCR as a compatibility fallback. Rate-limit registration and callback
  endpoints, and apply SSRF controls when fetching CIMD, PRM, AS metadata, or
  JWKS in custom infrastructure.
- Test missing, expired, wrong-issuer, wrong-audience, wrong-resource, missing
  scope, issuer-change, callback replay, and key-rotation cases before release.

## Normative references

- [MCP authorization (2026-07-28)][mcp-auth]
- [MCP authorization-server discovery][mcp-discovery]
- [MCP client registration][mcp-registration]
- [MCP authorization security considerations][mcp-security]
- [RFC 6750: Bearer Token Usage][rfc6750]
- [RFC 7591: Dynamic Client Registration][rfc7591]
- [RFC 7636: PKCE][rfc7636]
- [RFC 8252: OAuth for Native Apps][rfc8252]
- [RFC 8414: Authorization Server Metadata][rfc8414]
- [RFC 8707: Resource Indicators][rfc8707]
- [RFC 9207: Authorization Server Issuer Identification][rfc9207]
- [RFC 9700: OAuth 2.0 Security Best Current Practice][rfc9700]
- [RFC 9728: Protected Resource Metadata][rfc9728]
- [OAuth Client ID Metadata Document draft][cimd]

[mcp-auth]: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
[mcp-discovery]: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery
[mcp-registration]: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration
[mcp-security]: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations
[rfc6750]: https://www.rfc-editor.org/rfc/rfc6750
[rfc7591]: https://www.rfc-editor.org/rfc/rfc7591
[rfc7636]: https://www.rfc-editor.org/rfc/rfc7636
[rfc8252]: https://www.rfc-editor.org/rfc/rfc8252
[rfc8414]: https://www.rfc-editor.org/rfc/rfc8414
[rfc8707]: https://www.rfc-editor.org/rfc/rfc8707
[rfc9207]: https://www.rfc-editor.org/rfc/rfc9207
[rfc9700]: https://www.rfc-editor.org/rfc/rfc9700
[rfc9728]: https://www.rfc-editor.org/rfc/rfc9728
[cimd]: https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
"####]