rmcp-server-kit 3.13.0

Reusable MCP server framework with auth, RBAC, and Streamable HTTP transport (built on the rmcp SDK)
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
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
# Adopting & migrating `rmcp-server-kit`

This guide shows how to wire the standalone `rmcp-server-kit` crate into a
downstream project, and how to migrate across breaking major releases.

## Migrating to 3.13: origin validation narrowing

`3.13` tightens `allowed_origins` at several points. **No default changed** - an
empty allowlist still accepts requests without an `Origin` header - but two
classes of configuration and one class of request that previously passed now
fail.

### `allowed_origins` entries with a path, query, or fragment are a startup error

Previously such entries were accepted and simply never matched, so a
misconfigured allowlist started cleanly and rejected every browser request.
They now fail configuration validation with a message naming the field:

```text
allowed_origins entry "https://app.example.com/ui" must be scheme://host[:port]
(http or https), optionally with one trailing '/', or the literal "null"
```

Fix by reducing each entry to its bare origin (`https://app.example.com`). One
root trailing slash is tolerated (`https://app.example.com/`), and the literal
token `null` opts in to `Origin: null` (which is rejected otherwise).

**The same rule now runs in both public validators.** `validate_server_config`
(the TOML-facing validator used by `examples/config_file_server.rs`) previously
returned `Ok` for these entries while `serve()` rejected them at startup; both
now call the same helper. The same pass closed three more one-sided rules:
`validate_server_config` now also checks `max_request_body > 0`, `public_url`'s
scheme, and the security-header overrides, and `McpServerConfig::check` now
rejects an empty `admin_role`. Configs that hit any of those were already
rejected by `serve()`; only the earlier validator disagreed.

**Acceptance widened at the same time**, so most explicit allowlists keep
working: scheme/host comparison is case-insensitive, and an explicit default
port matches the implicit form (`https://x:443` is the same origin as
`https://x`). If both spellings are listed today, one entry is now redundant.

**Ports are strict.** Only ASCII digits without leading zeros are accepted, and
a configured origin without a port matches only the default port - unlike
rmcp's own validator, which treats an omitted port as a wildcard. A configured
`https://x` therefore does **not** match `Origin: https://x:444`; add
`https://x:444` explicitly if that is intended.

**Requests carrying duplicate `Origin` headers are rejected** with `403`:
`Origin` is a single-value field, and trusting the first of several values was
ambiguous.

### `with_max_request_body` above 4 MiB is now actually enforced

Separately: the public `max_request_body` cap is now propagated into rmcp's own
streamable-HTTP config, which previously kept its 4 MiB default. A deployment
that configured the knob above 4 MiB accepted only 4 MiB; it now accepts up to
the configured value. If the intent was to stay at 4 MiB, set the knob to
`4194304` (or lower) and keep any front-door WAF / reverse-proxy cap in sync -
the framework no longer rejects bodies between 4 MiB and the configured value.

## Migrating to 3.8: RFC 8693 token-exchange optionality

`3.8` contains **two source-breaking API changes**, shipped in a minor release as
a deliberate, documented exception to the "breaking changes bump major" policy.
Both affect only code that constructs `TokenExchangeConfig` in Rust.

**TOML configuration files require no change, and the token-exchange request
sent on the wire is byte-identical for any pre-3.8 configuration.**

### `TokenExchangeConfig::audience` is now `Option<String>`

RFC 8693 §2.1 marks `audience` OPTIONAL - only `grant_type`, `subject_token`,
and `subject_token_type` are REQUIRED. The crate previously made it mandatory
and always emitted it, so omission was unrepresentable and an authorization
server that rejects `audience`, or expects RFC 8707 `resource` instead, could
not be configured.

`audience` therefore left the constructor and became a setter:

```rust
// before
let tx = TokenExchangeConfig::new(
    token_url, client_id, Some(secret), None, "downstream-api".to_string(),
);

// after
let tx = TokenExchangeConfig::new(token_url, client_id, Some(secret), None)
    .with_audience("downstream-api");
```

To omit the parameter entirely, simply do not call `.with_audience(..)`.

`audience = ""` is now **rejected at startup**: an empty value is a malformed
request parameter, distinct from omission. Omit the key instead.

### `TokenExchangeConfig::new` now takes `impl Into<String>`

`token_url` and `client_id` changed from `String` to `impl Into<String>`, to match
the crate's `with_*` setters. `cargo-semver-checks` does not report this change, so
it is documented here rather than caught by tooling.

- A bare value - `&str`, `String`, `.to_string()`, or `.to_owned()` - compiles unchanged.
- Adding `.into()` to either argument now fails to compile with `E0283`, because the
  generic target type is ambiguous. This includes a `String` variable (`url.into()`),
  not only string literals. Drop the `.into()`:

```rust
// before
TokenExchangeConfig::new("https://idp/token".into(), "client".into(), None, None)
// after
TokenExchangeConfig::new("https://idp/token", "client", None, None)
```

### New optional parameters

`with_resource`, `with_scope`, and `with_requested_token_type` expose the
remaining RFC 8693 §2.1 OPTIONAL parameters. All default to omitted except
`requested_token_type`, which defaults to `access_token` - exactly what every
release before 3.8 always sent.

`resource` is validated as an RFC 8707 absolute URI with no fragment. It is
**unrelated** to `oauth.proxy.strip_resource_param`, which governs the OAuth
proxy endpoints, not token exchange. A custom `requested_token_type` is validated as
an absolute RFC 3986 URI (fragments permitted), so a typo such as `"acess_token"` is
rejected at startup rather than forwarded to the authorization server verbatim.

### New config keys are not backward-compatible with an older binary

`TokenExchangeConfig` - like every OAuth config struct - carries
`#[serde(deny_unknown_fields)]`. A pre-3.8 config parses cleanly on a 3.8 binary,
because missing keys fall back to their defaults, but the reverse does **not** hold: a
config that sets any new 3.8 key (`resource`, `scope`, `requested_token_type`, or the
OAuth-level `authorization_servers` / `allowed_algorithms`) is rejected at startup by a
**pre-3.8 binary**, which treats them as unknown fields.

Roll the binary and the config forward together: you cannot stage the new config
ahead of the deploy, nor roll the binary back while the new keys remain in place. If
you need a rollback window, add the keys only after every instance is on 3.8. A typo in
any key is likewise a hard startup error, not a silent skip.

## Migrating to 3.8: OAuth discovery metadata (RFC 9728 / RFC 8414)

Independently of token exchange, `3.8` makes the OAuth **discovery-metadata** documents
RFC 9728 / RFC 8414 conformant. **Inbound JWT validation is unchanged** - the token
`iss` is still validated against `oauth.issuer` - so most deployments need no action.
Two published values change, each with a legacy escape hatch, and exactly one topology
must act.

### Authorization Server Metadata `issuer`

Served at `/.well-known/oauth-authorization-server` (only when `oauth.proxy` is
configured), the published `issuer` is now this server's own public URL instead of the
upstream `oauth.issuer`. RFC 8414 §3.3 requires the `issuer` to match the origin the
document is served from, and §6.2 requires clients to reject a mismatch, so the old
value was unusable to conformant clients.

Restore the upstream value only if your IdP emits RFC 9207 `iss` in the authorization
response **and** your clients validate it:

```toml
[server.auth.oauth]
authorization_server_metadata_issuer = "https://upstream-idp.example.com"
```

### Protected Resource Metadata `authorization_servers`

Served unconditionally, `authorization_servers` is now resolved from topology: the
upstream `oauth.issuer` when `oauth.proxy` is absent, this server's public URL when it
is present.

**Action required for one topology: the facade.** An application that mounts its **own**
`/authorize` and `/token` through `McpServerConfig::with_extra_router` **without**
configuring `oauth.proxy` must set `authorization_servers` explicitly. The crate cannot
distinguish a facade from a plain resource server - both leave `oauth.proxy` unset - so by
default it advertises `oauth.issuer` (the upstream IdP). That is correct for a plain
resource server, but wrong for a facade, whose `/authorize` + `/token` live at this
server, not upstream.

A facade has two correct settings, and exactly one is required:

- **You know your public URL** (recommended). Advertise it so clients discover the
  facade's own endpoints. Use the URL clients actually reach, and keep it identical to
  `public_url`:

  ```toml
  [server.auth.oauth]
  authorization_servers = ["https://mcp.example.com"]   # this facade's public URL
  ```

- **You have no stable public URL** (for example behind a dynamic ingress you cannot name
  at startup). Omit the claim rather than advertise a wrong one. An empty list is a
  deliberate, RFC 9728 §3.2-compliant "no authorization server advertised here", **not** a
  misconfiguration: it passes startup validation, and clients fall back to other discovery
  (the `WWW-Authenticate` challenge, or out-of-band configuration):

  ```toml
  [server.auth.oauth]
  authorization_servers = []   # omit the claim; never advertise a URL you cannot stand behind
  ```

**`public_url` does not help a facade.** With `oauth.proxy` unset, `authorization_servers`
resolves to your explicit value or, if unset, to `oauth.issuer`; it never derives from
`public_url`. The `public_url` derivation described below applies only to the built-in
`proxy` topology and to the Authorization Server Metadata `issuer`, and the crate does not
serve that document without `oauth.proxy` anyway (your facade serves its own). So for a
no-proxy facade, `authorization_servers` is the only lever.

Both discovery URLs are validated at startup (parseable, no userinfo, scheme honouring
`allow_http_oauth_urls`, no literal-IP target); zero-valued claims are omitted rather than
emitted as `[]`; and Protected Resource Metadata is additionally served at the RFC 9728
§3.1 path `/.well-known/oauth-protected-resource/mcp`, with the root path kept as an alias.

### Both derive from `public_url`

With the built-in `oauth.proxy` mounted, the "this server's public URL" advertised as `authorization_servers` and
as the metadata `issuer` comes from `McpServerConfig::with_public_url`. When `public_url`
is unset it falls back to the bind address - behind a TLS-terminating proxy an internal
`http://` address - so the discovery documents advertise `authorization_servers`,
`issuer`, and endpoint URLs clients cannot reach, and the `WWW-Authenticate`
`resource_metadata` challenge degrades to a relative path. **Any proxied or public
deployment should set `public_url`**, and an explicit `authorization_servers` value
should match it.

## Migrating to 3.8: CRL fail-closed by default

This mTLS/CRL hardening is additive at the API level -- `cargo semver-checks`
reports no breaking change -- but it **alters two runtime defaults**. `cargo semver-checks` cannot
detect a behavioural default change, so read this section before upgrading.

### 1. `crl_deny_on_unavailable` now defaults to `true`

Previously, a client certificate advertising CRL distribution points was
**accepted** when its CRL could not be fetched or was not yet cached. That is
the exact condition an attacker holding a revoked certificate can induce, by
blocking reachability to the CA's CDP host.

From `3.8`, such a handshake is **rejected**, per RFC 5280 §6.3. Denial requires
*every* relevant CDP to be unavailable, not merely one -- otherwise blocking a
single mirror would become a denial-of-service vector.

**Who is affected:** deployments using mTLS with `crl_enabled = true` (the
default) whose client or CA certificates carry CDP extensions.

**Before upgrading, verify:**

- The CDP hosts in your client and CA certificates are reachable from the
  server's network, including through any egress proxy or firewall.
- `crl_max_cache_entries` (default `1024`) is large enough for your PKI.
  Fail-closed makes this cap **operationally visible**: a CDP that fetches
  successfully can still be rejected at the cache cap, leaving those
  handshakes denied. Large PKIs should raise it.
- The CRL SSRF guard permits your CDP hosts. Private, loopback, link-local,
  and cloud-metadata addresses are always rejected.

**To retain the previous behaviour**, opt out explicitly:

```toml
[server.auth.mtls]
crl_deny_on_unavailable = false
```

This is strongly discouraged: it accepts a revoked certificate whenever its
CRL is unreachable.

### 2. Secrets are redacted in `Debug` and log output by default

`ExchangedToken` (OAuth access tokens), the token-exchange claim log (`sub`,
`aud`, `azp`, `iss`), and `ToolCallContext` (tool arguments, identity, role,
`sub`) previously rendered their contents in plaintext. They now redact.

If you relied on that output for debugging, re-enable it per category:

```toml
[observability]
log_plaintext_oauth_tokens = false  # OAuth access tokens
log_oauth_claim_values     = false  # JWT sub / aud / azp / iss
log_tool_call_arguments    = false  # tool arguments and identity fields
```

or via `RMCP_SERVER_KIT__OBSERVABILITY__LOG_PLAINTEXT_OAUTH_TOKENS` and
friends.

> These switches are **process-wide, not per-server**. A process hosting more
> than one server shares one set. Enabling one writes secrets to your logs;
> intended for short-lived local debugging only.

### 3. `auth.mtls` without TLS is now a validation error

Configuring `auth.mtls` without both `tls_cert_path` and `tls_key_path` used to
start successfully with client-certificate authentication **silently disabled** --
a plaintext listener never performs a TLS handshake, so no client identity is
ever extracted. This combination is now rejected by
`McpServerConfig::validate()`. Supply both TLS paths, or remove `auth.mtls`.

### 4. Argument allowlists warn when `required = false`

An `ArgumentAllowlist` with a non-empty `allowed` list and `required = false`
constrains the argument only when the caller supplies it. If your tool
substitutes a default for a missing argument, a caller can bypass the allowlist
by omitting it. This now emits a startup warning naming the tool and argument.

Behaviour is unchanged in `3.8`. Prefer the constructor that fails closed:

```rust
ArgumentAllowlist::new_required("tool", "arg", vec!["allowed".into()]);
```

`required` defaults to `false` and **will keep doing so**. An earlier version of
this guide said the default would flip to `true` in `4.0`; that promise has been
withdrawn. Presence enforcement is opt-in by design, because an allowlist that
constrains a supplied value is a legitimate configuration and flipping the
default would silently turn previously-allowed traffic into `403`s with no
compile error. Set `required = true` (or use `new_required`) wherever omitting
the argument must be rejected -- there is no future release in which that
happens automatically.

### 5. Certificates advertising more than 64 CDP URLs are rejected

**Impact:** a client certificate carrying more than **64** distinct CRL
distribution point URLs is now rejected as malformed.

**This applies in both fail-open and fail-closed modes and has no opt-out.**
`crl_deny_on_unavailable = false` opts out of *revocation-unavailability*
denials; it does not opt out of malformed-certificate rejection, because the
amplification being bounded is paid identically in fail-open mode.

**Action required:** none for conforming certificates. RFC 5280
[4.2.1.13](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13) treats
multiple URIs inside one `DistributionPoint` as alternative ways to obtain the
same CRL, so a conforming certificate needs only a handful. If you genuinely
issue client certificates with more than 64 distinct CDP URLs, they will now be
rejected; reduce the count or use CRL partitioning.

**Observable signature:** a throttled `crl_cdp_url_cap_exceeded` WARN naming
the observed count and the cap.

### 6. `CrlSet::cache` and the ungated test constructors are deprecated

**Impact:** `CrlSet::cache`, `CrlSet::__test_with_prepopulated_crls`, and
`CrlSet::__test_with_kept_receiver` now carry `#[deprecated(since = "3.8.0")]`.

> **This is not a semver-breaking change, but it CAN break your build.** A
> downstream crate compiling with `-D warnings` (or `#![deny(warnings)]`) will
> fail until it adds `#[allow(deprecated)]` at the call sites. This is a
> deliberate, documented cost of signalling the 4.0 change early.

**Action required:**

- **Reading** `CrlSet::cache` remains safe and behaves as before.
- **Mutating** `CrlSet::cache` out of band is now *detected and denies
  handshakes* when `crl_deny_on_unavailable = true`. Use
  `force_refresh` or the normal discovery/refresh path instead. If you were
  mutating the cache directly in tests, expect handshake denials and a
  throttled `crl_cache_out_of_band_mutation` WARN.
- If you call the `__test_*` constructors from your own integration tests, add
  a file-level `#![allow(deprecated, reason = "...")]`.

**Planned for 4.0:** `cache` becomes `pub(crate)`, and both `__test_*`
constructors become gated behind the `test-helpers` feature. Deprecating them
now is the only non-breaking way to give downstream users notice.

## Migrating from 3.4 to 3.5

`3.5` is additive at the API level -- `cargo semver-checks` reports no breaking
change, no default values were altered, and no code change is required to
upgrade. Existing configurations parse and behave identically to 3.4.x unless
you opt in to the new features.

### No action required

Upgrade by bumping the version constraint:

```toml
rmcp-server-kit = "3"
```

Run `cargo update -p rmcp-server-kit` and verify the full test suite passes.
Nothing else is needed.

### Opting in to environment variable overrides

3.5 adds three opt-in methods that layer env overrides onto already-constructed
config structs: `ServerConfig::apply_env_overrides`,
`ObservabilityConfig::apply_env_overrides`, and `RbacConfig::apply_env_overrides`.
None are called automatically by `serve()` or `validate()`.

To opt in, call the methods after TOML deserialization and before
`apply_to_mcp_config`. See
[Environment variable overrides (opt-in)](GUIDE.md#environment-variable-overrides-opt-in)
in the guide for the full call order, the 14-variable reference table, secret
handling rules, and the metrics-wiring caveat.

## Migrating from 3.3 to 3.4

`3.4` is additive at the API level - `cargo semver-checks` reports no breaking
change, no default values were altered, and no code change is required to
upgrade. Existing configurations parse and behave identically to 3.3.x unless
you opt in to the new features.

### No action required

Upgrade by bumping the version constraint:

```toml
rmcp-server-kit = "3"
```

Run `cargo update -p rmcp-server-kit` and verify the full test suite passes.
Nothing else is needed.

### Opting in to TOML-driven transport configuration

3.4 adds `ServerConfig::apply_to_mcp_config`, which bridges the existing TOML
schema into `serve()`. To use it, replace your manual field-wiring code with a
single bridge call. See
[Bridging TOML config to `McpServerConfig`](GUIDE.md#bridging-toml-config-to-mcpserverconfig)
in the guide for a worked example and a description of replacement semantics.

If you use the `[server.security_headers]` TOML table for the first time, review
the twelve available keys and their built-in defaults in the
[Customising security headers](GUIDE.md#customising-security-headers) section.
Any header you do not mention keeps its current default, so you can opt in
incrementally.

## Migrating from 3.2 to 3.3

`3.3` is additive at the API level - `cargo semver-checks` reports no breaking
change, and no code change is required to upgrade. It does, however, tighten
three **runtime behaviours** as part of a security-hardening pass. Each is a
deliberate fail-closed change with no opt-out: an escape hatch would simply be
the original weakness behind a config flag.

Review these before rolling out.

### 1. Prometheus `path` label values changed (`metrics` feature only)

Most likely to affect you, and the only one that can break silently.

`http_requests_total` and `http_request_duration_seconds` previously used the
**raw request path** as the `path` label. Any unauthenticated client could
therefore mint an unbounded number of Prometheus time series by requesting
random paths, growing in-process memory until exhaustion.

Label values now come from a closed set:

| Request | Old `path` label | New `path` label |
|---------|------------------|------------------|
| `GET /healthz` | `/healthz` | `/healthz` |
| `POST /mcp` | `/mcp` | `/mcp` |
| `POST /mcp/<session>` | `/mcp/<session>` | `/mcp` |
| `GET /does-not-exist` | `/does-not-exist` | `<unmatched>` |
| `FROBNICATE /healthz` | `FROBNICATE` (method label) | `OTHER` |

Label **names** are unchanged, so panels keyed on `path` or `method` keep
working. **Action:** if any dashboard, recording rule, or alert matches on a
raw unmatched path, update it to `<unmatched>`, and update anything matching
per-session `/mcp/...` paths to `/mcp`.

### 2. `Forwarded` header parsing requires balanced quotes

RFC 7239 §4 defines a parameter value as `token / quoted-string`, and a
quoted-string requires balanced `DQUOTE`. Values such as `for="203.0.113.9`
(lone leading quote), `for=203.0.113.9"` (lone trailing), and
`for="""203.0.113.9"""` were previously normalised into a valid address; they
are now rejected as malformed and client-IP resolution falls back to the direct
peer.

This removes a parser differential between `rmcp-server-kit` and the upstream
proxy, which matters because the resolved client IP feeds per-IP rate limiting
and operator allowlists.

**Action:** none, if your proxy emits RFC-compliant headers - well-formed
quoted values including `for="[2001:db8::1]:443"` are unaffected. If you see a
rise in fallback-to-peer resolution after upgrading, your proxy is emitting
malformed `Forwarded` values and should be fixed.

### 3. RBAC rejects a non-string `arguments.host`

The host was previously read with `as_str()`, so an array, object, number,
bool, or null yielded `None` and the request was evaluated **without** the
role's `hosts` glob restrictions. A caller could opt out of host restrictions
simply by changing the argument's shape.

A present-but-non-string `host` is now denied with 403.

**Action:** none for well-behaved clients. A client sending
`"host": ["prod-1"]` and receiving 200 before will now receive 403 - which was
the vulnerability. An **absent** `host` is unchanged and still evaluated
without host restrictions, so genuinely hostless tools (`ping`, `list_hosts`)
continue to work.

### Also in 3.3 (no action required)

- `ArgumentAllowlist::required` - new opt-in field, defaults to `false`.
  Existing configurations parse and behave identically. See
  [`GUIDE.md`]GUIDE.md for when to enable it.
- mTLS CRL distribution points whose first fetch fails are now retried on a
  later handshake instead of being suppressed for the process lifetime.
- OAuth proxy requests drop caller-supplied client-authentication parameters
  before injecting the configured ones.
- Graceful shutdown lets in-flight MCP sessions finish within
  `shutdown_timeout` instead of cancelling them when the drain begins.

## Migrating from 2.x to 3.0

`3.0` upgrades the underlying MCP SDK from `rmcp` 2.x to **`rmcp` 3.0**.
Because your crate depends on `rmcp` **directly** (you implement
`rmcp::handler::server::ServerHandler` and use `rmcp::model` types), this is
a breaking change you must coordinate:

1. **Bump `rmcp` in lockstep.** In your own `Cargo.toml`, change
   `rmcp = "2"` to `rmcp = "3"` (keep your existing features). Your `rmcp`
   major must match the one `rmcp-server-kit` links against, or the
   `ServerHandler` trait will not unify.

2. **Update manual handler return types - only if you override them.**
   rmcp 3.0 makes `tools/call`, `prompts/get`, and `resources/read`
   MRTR-aware (SEP-2322). If your `ServerHandler` overrides these methods,
   change the return type to the new response enum and wrap your existing
   result with `.into()`:

   ```rust
   // 2.x
   async fn call_tool(&self, req: CallToolRequestParams, cx: RequestContext<RoleServer>)
       -> Result<CallToolResult, ErrorData>
   { Ok(CallToolResult::success(content)) }

   // 3.0
   async fn call_tool(&self, req: CallToolRequestParams, cx: RequestContext<RoleServer>)
       -> Result<CallToolResponse, ErrorData>
   { Ok(CallToolResult::success(content).into()) }
   ```

   The same pattern applies to `get_prompt` (`GetPromptResponse`) and
   `read_resource` (`ReadResourceResponse`). Handlers that only implement
   `get_info` - the common case - need **no** code change beyond step 1.

3. **MSRV.** rmcp 3.0 requires Rust ≥ 1.88; `rmcp-server-kit` targets
   1.98, so no action is needed.

The `rmcp-server-kit` public API surface (config, auth, RBAC, transport)
is otherwise unchanged for 3.0.

## 1. Add the dependency

### crates.io (recommended)

Use a caret range so patch and minor releases flow in automatically:

```toml
[dependencies]
rmcp-server-kit = { version = "3", features = ["oauth"] }
```

Avoid the exact-version pin (`version = "=1.6.0"`); it prevents security
patches from reaching your build.

### Git dependency (development / pre-release)

Pin to a tagged release:

```toml
[dependencies]
rmcp-server-kit = { git = "https://github.com/andrico21/rmcp-server-kit", tag = "3.0.0", features = ["oauth"] }
```

## 2. Workspace integration

If your project is a Cargo workspace, add your application crate as a
member and let it depend on `rmcp-server-kit` from crates.io:

```toml
[workspace]
members = ["my-app"]
resolver = "3"
```

`rmcp-server-kit` is published as a standalone crate; it is **not**
intended to be vendored as a workspace member of downstream projects.

## 3. Lints

`rmcp-server-kit` owns its own `[lints]` table and enforces a strict
internal lint set (no `unwrap` / `expect` / `panic` / `println!` in
production paths, `unsafe_code = "forbid"`, `missing_docs = "warn"`).
Downstream crates are free to keep or promote their own workspace
lints independently - the two lint tables do not interact.

## 4. Build & verify

```bash
cargo update -p rmcp-server-kit
cargo build --all-features
cargo test --all-features
```

If you observe a different `rmcp` version resolution than expected, pin
`rmcp` in your own `Cargo.toml` to match the version declared in
`rmcp-server-kit`'s `[dependencies]`.

## 5. Feature flags

| Feature   | Meaning                                                  |
|-----------|----------------------------------------------------------|
| `oauth`   | Enables OAuth 2.1 JWT validation and token exchange.     |
| `metrics` | Exposes a Prometheus registry and `/metrics` endpoint.   |

Both are opt-in to keep the default dependency footprint small.

## 6. Minimum supported Rust

`rmcp-server-kit` targets stable Rust **1.98** or newer (`edition = "2024"`).
Bumping the MSRV is a minor-version change under the project's SemVer
policy.