Skip to main content

cimdkit

OAuth Client ID Metadata Documents (CIMD) for Python authorization servers.

MCP clients such as Claude Code and VS Code no longer register with your OAuth server: they send a URL as their client_id, and your server has to fetch that URL, validate the JSON it finds there and remember it. Doing that safely is fiddly (SSRF, redirects, exact-match rules, caching, the loopback port exception), and every server is writing it again from scratch. cimdkit does it in one call, with no dependencies.

pip install cimdkit

Python 3.9+. Works with any web framework and any OAuth library (Authlib, FastMCP's OAuth proxy, your own).

Use it in your /authorize endpoint

from cimdkit import ClientResolver, CIMDError, Policy, SERVER_METADATA, is_client_id_url

resolver = ClientResolver(Policy(
    forbidden_client_ids=("https://auth.example.com",),  # your own issuer
    auth_methods=("none", "private_key_jwt"),
))

def authorize(params):
    client_id = params["client_id"]
    if not is_client_id_url(client_id):
        return handle_preregistered_client(params)       # your existing clients
    try:
        client = resolver.resolve(client_id)             # fetch + validate + cache
        redirect_uri = client.check_redirect_uri(params.get("redirect_uri"))
    except CIMDError as err:
        # Never redirect on these errors: show them to the user.
        return render_error(err.to_oauth_error(), status=err.status_code)
    return render_consent(name=client.display_name, host=client.host, logo=client.logo_uri,
                          redirect_uri=redirect_uri)

# And advertise support in /.well-known/oauth-authorization-server:
metadata.update(SERVER_METADATA)   # {"client_id_metadata_document_supported": True}

At the token endpoint, call resolver.resolve(client_id) again (it is served from the cache) and use client.token_endpoint_auth_method: "none" means a public PKCE client, so reject any client authentication it sends; "private_key_jwt" means verify its assertion against client.jwks or client.jwks_uri. On a TransientFetchError answer 503; on any other CIMDError answer invalid_client, and if err.revoke_tokens is true, revoke the client's refresh tokens (the document now names a different client).

What it checks

The URL (check_client_id_url): https, a path other than /, no userinfo, no ./.. segments (including %2e), no fragment, a valid port. Compared as an exact string, never normalized.

The fetch (fetch_document):

  • Every address the host resolves to must be public. Loopback, private, link-local (including the cloud metadata address 169.254.169.254), CGNAT, multicast and reserved ranges are refused, also when hidden as IPv4-mapped IPv6, NAT64 or 6to4.
  • The connection goes to the exact address that was checked, with SNI and certificate checks for the real host name, so DNS rebinding cannot swap the address between check and connect.
  • Redirects are not followed. Only 200 is accepted; 5xx, 408 and 429 are reported as transient.
  • Accept: application/json, at most 5 KB read, one overall deadline, application/json (or application/*+json) content type, and a JSON object with no duplicate keys.

The document (validate_document):

  • client_id must exactly equal the URL it was fetched from.
  • No client_secret, no shared-secret auth methods (client_secret_basic, _post, _jwt), no private key material in jwks, not both jwks and jwks_uri.
  • With no auth method declared, the method is none (not RFC 7591's client_secret_basic). With token_endpoint_auth_methods_supported, the client's first method that you support is chosen.
  • grant_types and response_types are intersected with what you support; an empty result is an error.
  • redirect_uris: absolute, no fragment, no javascript:/data:/file:, plain http only on loopback. Private-use schemes for native apps (com.example.app:/cb) are allowed.
  • Unknown properties are ignored, not rejected. Informative URLs (logo_uri, client_uri...) that are not https are dropped with a warning instead of failing the whole sign-in.

Redirect URIs (client.check_redirect_uri): exact match, except that http://127.0.0.1, http://[::1] and http://localhost redirects may use any port, as RFC 8252 and RFC 9700 require for native apps and CLIs that listen on a random port.

Caching (ClientResolver): valid documents are kept for as long as Cache-Control (s-maxage, max-age, Age, no-store...) or Expires allow, clamped to [min_ttl, max_ttl] (default 0 s to 24 h, 5 minutes when the response says nothing). Errors and invalid documents are never cached. A transient failure keeps serving the last good document; a terminal failure drops it.

Your policy (Policy): supported grant types, response types and auth methods, ids the client_id must never equal (your issuer, trusted resource servers), allowed_hosts ("*.example.com") to accept only trusted client domains, same-origin redirects, and whether localhost counts as loopback.

Check your own client's document

If you publish a CIMD for your MCP client, check it the way servers will:

$ cimdkit check https://app.example.com/oauth/client.json --redirect-uri http://127.0.0.1:53682/callback
OK  https://app.example.com/oauth/client.json
  name           Example Agent  (host app.example.com)
  auth method    none
  grant types    authorization_code, refresh_token
  redirect_uri   http://127.0.0.1/callback
  cache          3600 s

--file client.json checks a local file before you deploy it, and --json prints the result for CI. Or build the document in Python and get an error if any server would reject it:

from cimdkit import make_document

doc = make_document("https://app.example.com/oauth/client.json",
                    client_name="Example Agent",
                    redirect_uris=["http://127.0.0.1/callback"],
                    logo_uri="https://app.example.com/logo.png")

For development

ClientResolver(allow_loopback=True) (or cimdkit check --allow-loopback) lets a server on your machine fetch documents from 127.0.0.1/::1. The spec forbids this in production.

Limits

  • It resolves and validates clients. It does not issue codes or tokens, verify private_key_jwt assertions, or fetch jwks_uri and logos for you.
  • The fetch connects directly and ignores HTTPS_PROXY. Pass your own transport= to ClientResolver/fetch_document if your server must use a proxy.
  • The cache is in memory, per process. Pass fetch= or wrap resolve to share one.
  • It follows draft-ietf-oauth-client-id-metadata-document as of November 2026. The draft can still change.

Why this exists

MCP's authorization spec made CIMD the default way for clients to identify themselves (SEP-991), and authorization servers in every language are being asked to support it:

The first implementations keep hitting the same bugs, each one a test here: unknown properties rejected (keycloak#51236), loopback redirect ports (claude-code#37747), private-use redirect schemes rejected (fastmcp#6336), a malformed port causing a 500 (fastmcp#5764), no way to limit CIMD to trusted domains (fastmcp#6153), and partial client_id URL validation (python-sdk#3768).

License

MIT

Metadata

Release files for cimdkit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cimdkit 0.1.0
File Size Uploaded Provenance Actions
cimdkit-0.1.0.tar.gz 26.1 kB Without provenance Details for cimdkit-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cimdkit 0.1.0
File Interpreter ABI Platform Actions
cimdkit-0.1.0-py3-none-any.whl Python 3 none any Details for cimdkit-0.1.0-py3-none-any.whl

Total release size: 47.9 kB

Release files / cimdkit-0.1.0.tar.gz

Download URL cimdkit-0.1.0.tar.gz
Size 26.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5db79d7259a013f35a1354328156f0f3c68f5301e1fdda1b995efbb01d7dc33e
BLAKE2b-256 checksum
How to use checksums
126367582c75de2e26bf65d8f7c5ffe64de9cc655c48b344f6aef8dca8957bcf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / cimdkit-0.1.0-py3-none-any.whl

Download URL cimdkit-0.1.0-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
465a9d2277e249b5a127e6e8ff2dc238be164c691ffcb77df52c796f4666c5ac
BLAKE2b-256 checksum
How to use checksums
6312a55718ae135ad41359a8fc24868e14b40091fe3987b330bd6f002a7f388e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for cimdkit 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page