Auth-hardening conformance checks
The 2026-07-28 spec tightens six aspects of the OAuth / OIDC surface a Model Context Protocol server may expose. Each tightening is its own SEP. mcptest ships a library of pure functions that take the relevant metadata input and return one of pass, fail, or not-applicable, so a conformance run can score a server against the set without a custom auth client per check.
This page is the operator-level reference: what each check asks, what input it consumes, and what a violation looks like. The implementation lives in mcptest_core::auth::conformance.
The six checks
| SEP | Function | What it asks |
|---|---|---|
| 2468 | check_iss_present | Does the authorization response carry iss, and does it match the configured issuer (RFC 9207)? |
| 837 | check_dcr_application_type | Did the DCR registration declare application_type, and does it match the client's deployment shape (native for CLI, web for browser apps)? |
| 2352 | check_as_bound_credentials | If the resource migrated to a new authorization server, did the client re-register before reusing its credentials? |
| 2207 | check_oidc_refresh | If offline_access was granted, did the client rotate the refresh token on use? |
| 2350 | check_scope_step_up_accumulation | Did the step-up grant accumulate the previously held scopes with the newly requested ones, instead of silently downgrading? |
| 2351 | check_well_known_suffix | Is the .well-known URI a terminal /.well-known/<id> form, with no path component after the identifier? |
Outcome shape
Every check returns a CheckOutcome:
pub enum CheckOutcome {
Pass,
Fail { reason: String },
NotApplicable { reason: String },
}
Pass means the metadata satisfies the rule. Fail carries a one-line reason the reporter renders next to the check name. NotApplicable is for checks that do not exercise against the captured input (for example, check_oidc_refresh returns NotApplicable when the grant did not include offline_access).
What v1 does and does not do
v1 ships the library: each check is a pure function that takes a typed input struct and returns the outcome.
Bridge to a compliance report
mcptest_core::compliance::auth_hardening is the bridge between captured OAuth/OIDC metadata and the six checks. The runner populates an AuthHardeningProbe from a real OAuth handshake:
let probe = AuthHardeningProbe {
iss: Some(IssCapture {
iss_from_response: Some(callback.iss.clone()),
expected_issuer: as_metadata.issuer.clone(),
}),
dcr: Some(DcrCapture {
declared: registration.application_type.clone(),
is_native_client: true,
}),
// ... three more optional captures ...
well_known_url: Some(resolved_well_known.clone()),
};
let report = run_checks(&probe);
run_checks(probe) returns an AuthHardeningReport with one AuthHardeningRow per SEP. Each row carries the SEP id, a human-readable name, and the underlying CheckOutcome. A missing field on the probe maps to NotApplicable rather than a failure so a partial capture (a flow that did not exercise the OIDC refresh, say) still produces a usable scorecard.
Planned follow-up
- Capture wiring: hook the connector's OAuth flow so an
AuthHardeningProbeis populated automatically on everymcptest compliance --from-suiterun that touches a URL server. - Scorecard section: fold the auth-hardening report into the compliance rubric's
TransportAuthsection so the report's letter grade reflects the pack. - Renderer support: surface the per-row pass/fail/N-A in the pretty + JSON renderers without forcing the operator to read the JSON.
Cross-references
- covers the existing OAuth 2.1 + PKCE login flow these checks validate.
- covers the URL-server transport these checks gate access to.
- RFC 9207 (
issparameter), RFC 8414 (AS metadata at/.well-known).