---
title: "Client ID Metadata Documents (CIMD): A Tutorial for MCP Clients · Highflame"
description: "CIMD lets an MCP client use an HTTPS URL as its client_id, with no registration. Write the document, host it, and connect a stock MCP client to a server."
doc_version: "0.1.0"
last_updated: "2026-10-22T05:17:01.408Z"
canonical: "https://www.highflame.com/learn/client-id-metadata-documents"
---

[Home](/index.md)·[Learn](/learn.md)·Client ID Metadata Documents (CIMD)

Reference

# Client ID Metadata Documents (CIMD)

Client ID Metadata Documents (CIMD) let an OAuth client use an HTTPS URL as its client\_id, where the URL serves a small JSON document that describes the client. The authorization server fetches and checks that document instead of storing a registration, and MCP prefers it for a client meeting a server for the first time.

Highflame EngineeringLast reviewed October 2026

Key takeaways

-   With CIMD, the client\_id is an HTTPS URL and the JSON document at that URL describes the client. The authorization server stores no registration.
-   The server accepts the document only if it names its own URL and lists the redirect URI the client asked for.
-   Host the document on any static HTTPS origin you control, then pass its URL to the MCP SDK as client\_metadata\_url.
-   CIMD identifies the client. It does not sign the user in, and it does not decide which tools the client may call.

## The problem: one registration per install

An MCP client is installed per desktop, and it meets authorization servers it has never seen. Under Dynamic Client Registration (RFC 7591), each install posts its metadata to each server, and each server stores a client record it keeps forever. A company with 2,000 developers, each running two MCP clients against ten servers, ends up with 40,000 client registrations. None of them can be told apart from an impostor, because the registration endpoint takes the client’s claimed `client_name` on trust, and anyone can post `"client_name": "Claude Desktop"`.

Client ID Metadata Documents remove the record. The client publishes a JSON document at an HTTPS URL it controls, and that URL is its `client_id`. The authorization server reads the document when the flow starts and stores nothing. The client’s identity now rests on control of its domain, which TLS and DNS already verify.

The standard is [`draft-ietf-oauth-client-id-metadata-document`](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/). MCP adopted it as [SEP-991](https://modelcontextprotocol.io/seps/991-enable-url-based-client-registration-using-oauth-c), and the current MCP specification deprecates Dynamic Client Registration in favor of it.

## What the document holds

This is a complete document for a desktop MCP client, served at `https://app.example.com/oauth/client.json`:

```
{
  "client_id": "https://app.example.com/oauth/client.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://app.example.com",
  "redirect_uris": [
    "http://127.0.0.1:3000/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

Field

What the authorization server does with it

`client_id`

Compares it with the URL it fetched. They must be identical.

`client_name`

Shows it to the user on the consent screen.

`redirect_uris`

Sends authorization codes only to these addresses. Required.

`grant_types`

Must include `authorization_code`. Add `refresh_token` if you want refresh tokens.

`token_endpoint_auth_method`

`none` makes a public client that relies on PKCE. `private_key_jwt` makes a confidential one (see below).

## The self-reference rule

The `client_id` inside the document must equal the URL the document was fetched from, character for character. This rule is what makes the URL an identity. A document at `https://evil.example/client.json` cannot claim to be `https://app.example.com/oauth/client.json`, because its own `client_id` field would not match the address the server fetched.

The second guard is `redirect_uris`. Anyone can type a published `client_id` URL into an authorization request. The server will still send the code only to a redirect URI listed in the real document, so an attacker who copies your URL cannot collect your users’ codes on the attacker’s own host.

## Host it

Any static HTTPS origin you control works: your product’s website, a docs site, or a storage bucket behind your own domain. Put the file at a stable path:

```
public/
  oauth/
    client.json   ->  https://app.example.com/oauth/client.json
```

Keep these rules, because authorization servers enforce them:

-   Use `https`, a non-root path, and no query string, fragment, user info, or `.` and `..` segments.
-   Answer `200` with the JSON directly. A redirect is a failed lookup, not a pointer to follow.
-   Keep it small. ZeroID, for example, refuses documents over 5 KiB by default.
-   Set a `Cache-Control: max-age`. Servers cache the document, and a short `max-age` lets a change to your `redirect_uris` take effect sooner.
-   Do not use a host where other people can publish paths, such as a shared raw-file CDN. An authorization server that allow-lists your host would admit them too.

Then check what a server will see:

```
curl -sS https://app.example.com/oauth/client.json | jq -r .client_id
# https://app.example.com/oauth/client.json
```

## Connect a stock MCP client

The official MCP Python SDK supports CIMD through one argument: `client_metadata_url`. The code below uses `mcp` 2.x (`pip install mcp`). It opens the browser for sign-in and catches the code on the loopback redirect URI that the document lists, which is how desktop clients receive authorization codes.

```
import asyncio
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

import httpx2
from mcp.client import Client
from mcp.client.auth import OAuthClientProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import AuthorizationCodeResult, OAuthClientMetadata

MCP_URL = "https://mcp.example.com/mcp"
CLIENT_ID_URL = "https://app.example.com/oauth/client.json"  # your hosted document
REDIRECT_URI = "http://127.0.0.1:3000/callback"              # listed in the document


class MemoryStorage:
    """Keeps tokens in memory. Use a keychain or file in a real client."""

    def __init__(self):
        self.tokens = None
        self.client_info = None

    async def get_tokens(self):
        return self.tokens

    async def set_tokens(self, tokens):
        self.tokens = tokens

    async def get_client_info(self):
        return self.client_info

    async def set_client_info(self, client_info):
        self.client_info = client_info


async def open_browser(authorization_url: str) -> None:
    webbrowser.open(authorization_url)


async def wait_for_code() -> AuthorizationCodeResult:
    """Serve one request on the loopback redirect URI and read the code from it."""
    params: dict[str, str] = {}

    class Callback(BaseHTTPRequestHandler):
        def do_GET(self):
            query = parse_qs(urlparse(self.path).query)
            params.update({k: v[0] for k, v in query.items()})
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"Signed in. You can close this tab.")

        def log_message(self, *args):
            pass

    server = HTTPServer(("127.0.0.1", 3000), Callback)
    await asyncio.to_thread(server.handle_request)
    server.server_close()
    return AuthorizationCodeResult(code=params["code"], state=params.get("state"), iss=params.get("iss"))


async def main() -> None:
    oauth = OAuthClientProvider(
        server_url=MCP_URL,
        client_metadata=OAuthClientMetadata(
            client_name="Example MCP Client",
            redirect_uris=[REDIRECT_URI],
            grant_types=["authorization_code", "refresh_token"],
            response_types=["code"],
            token_endpoint_auth_method="none",
        ),
        storage=MemoryStorage(),
        redirect_handler=open_browser,
        callback_handler=wait_for_code,
        client_metadata_url=CLIENT_ID_URL,
    )
    async with httpx2.AsyncClient(auth=oauth) as http:
        async with Client(streamable_http_client(MCP_URL, http_client=http)) as client:
            tools = await client.list_tools()
            print("tools:", [tool.name for tool in tools.tools])


asyncio.run(main())
```

The SDK does the discovery for you. The first request returns `401` with a pointer to the server’s protected resource metadata, which names the authorization server. The SDK then reads that server’s metadata, and it uses your URL as the `client_id` only when the metadata says `client_id_metadata_document_supported: true`. When the flag is missing, the SDK falls back to Dynamic Client Registration, so check it first:

```
curl -sS https://auth.example.com/.well-known/oauth-authorization-server \
  | jq .client_id_metadata_document_supported
# true
```

FIG · CIMD CLIENT ONBOARDING A URL AS THE CLIENT\_ID · NOTHING STORED

## What the authorization server checks

A server that accepts CIMD is fetching a URL that anyone can hand it, so the checks matter as much as the document. A careful server does all of these:

1.  **Checks the URL shape** before fetching anything: `https`, a path, no query or fragment, no user info, no dot segments.
2.  **Applies an optional host allow-list.** A deployment that admits only pre-approved clients lists the publisher hosts it trusts. An empty list admits any public HTTPS host.
3.  **Fetches safely.** It resolves the host once, refuses private, loopback, and cloud-metadata addresses, follows no redirects, and caps the size and the time.
4.  **Enforces self-reference.** The `client_id` field must equal the fetched URL.
5.  **Validates the redirect URIs.** Each one must be `https`, loopback `http`, or a private-use scheme for native apps. The requested `redirect_uri` must match the list exactly, except that a loopback callback may use any port, as RFC 8252 allows for desktop apps.
6.  **Checks the grants.** `authorization_code` must be present. ZeroID ignores grants it does not support instead of rejecting the document, so one document can serve every server you talk to.
7.  **Caches the result**, for successes and failures, so a busy client or a dead URL does not cause a fetch on every request.

## Public or confidential: which client you get

The document decides. With `"token_endpoint_auth_method": "none"`, you get a public client: no secret, and PKCE proves that the party finishing the flow is the party that started it. That is the right choice for a desktop install.

If you want the client authenticated, not only identified, declare `private_key_jwt` and publish exactly one key set:

```
{
  "client_id": "https://app.example.com/oauth/client.json",
  "client_name": "Example MCP Client",
  "redirect_uris": ["http://127.0.0.1:3000/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks_uri": "https://app.example.com/oauth/jwks.json"
}
```

At the token endpoint the client then signs a `client_assertion` with one of those keys, and the server refuses the request without it. Prefer `jwks_uri` over an inline `jwks` if you rotate keys: the server caches the document, but it refreshes the key set behind `jwks_uri` on its own schedule. No CIMD client ever gets a shared secret, because there is no registration response to deliver one in.

## Where CIMD stops

CIMD answers one question: which client is this? It does not sign the user in. The authorization server still runs its own login, and with [Enterprise Managed Authorization](/learn/enterprise-managed-authorization.md) that login is your corporate identity provider deciding which MCP servers the user’s agent may reach. CIMD also says nothing about what the client may do once it holds a token. A valid token for the right server can still be steered into the wrong tool call, which is why [MCP authorization](/learn/mcp-authorization.md) has to be paired with per-call authorization at runtime.

One more limit: a public CIMD client has no proof of possession. Anyone can present your URL. The redirect URIs keep the codes out of an attacker’s hands, but if your document lists only a loopback callback, a code goes to whichever machine started the flow. If that matters for your client, publish keys and use `private_key_jwt`.

## How Highflame supports CIMD

ZeroID, Highflame’s open-source identity engine, implements CIMD and turns it on by default. It accepts public clients that use PKCE and confidential clients that use `private_key_jwt` with `jwks` or `jwks_uri`. It never writes a CIMD client to its database, and a deployment can limit which hosts may publish documents with an allow-list. The [ZeroID CIMD reference](https://github.com/highflame-ai/zeroid/blob/main/docs/cimd.md) covers every check and setting. To confirm that any authorization server accepts CIMD, including one you run, look for `client_id_metadata_document_supported` in its metadata.

## Frequently asked questions

What is a Client ID Metadata Document (CIMD)?

A small JSON document that an OAuth client publishes at an HTTPS URL it controls. The URL itself is the client's client\_id. When the client starts an authorization flow, the authorization server fetches the document, checks it, and uses it as the client's registration for that flow. The standard is draft-ietf-oauth-client-id-metadata-document, and MCP adopted it as SEP-991.

How is CIMD different from Dynamic Client Registration?

Dynamic Client Registration (RFC 7591) has every client install POST its metadata to every authorization server it meets, and the server stores a record for each one. It also takes the client's claimed name on trust. With CIMD the server stores nothing: it reads the client's metadata from the client's own domain each time, so the identity rests on who controls that domain. The current MCP specification deprecates Dynamic Client Registration in favor of CIMD.

What stops someone from presenting another client's client\_id URL?

The redirect\_uris in the document. Anyone can present a published URL, but the authorization server only sends authorization codes to redirect URIs that the document lists. An attacker who copies a legitimate client's URL cannot receive that client's codes on the attacker's own callback host.

Where can I host a CIMD document?

On any static HTTPS origin you control, such as your product's website or a storage bucket behind your own domain. The URL must use https, have a path, and carry no query string or fragment, and the server must answer with the JSON directly, not with a redirect. Avoid hosts where other people can publish paths.

How does an MCP client know whether a server supports CIMD?

From the authorization server's metadata. A server that accepts CIMD sets client\_id\_metadata\_document\_supported to true. The official MCP Python SDK checks this flag: it uses your client\_metadata\_url when the flag is true and falls back to Dynamic Client Registration when it is not.

Is a CIMD client public or confidential?

Either. A document that declares token\_endpoint\_auth\_method none is a public client, and PKCE protects its flow. A document that declares private\_key\_jwt and publishes a key set (jwks or jwks\_uri) is a confidential client, and it must sign a client assertion at the token endpoint. A CIMD client never has a shared secret.

Highflame Engineering

Engineering and research on agent identity, runtime policy, and securing autonomous AI at Highflame.

▸ Related

-   [MCP Authorization (guide)](/learn/mcp-authorization.md)
-   [Enterprise Managed Authorization (guide)](/learn/enterprise-managed-authorization.md)
-   [CIMD in ZeroID (reference)](https://github.com/highflame-ai/zeroid/blob/main/docs/cimd.md)
-   [draft-ietf-oauth-client-id-metadata-document (IETF)](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)
-   [SEP-991: URL-based client registration (MCP)](https://modelcontextprotocol.io/seps/991-enable-url-based-client-registration-using-oauth-c)

See agent governance against your own agents.

[Book a demo](/contact.md) [Explore the platform](/platform.md)

## Sitemap

Full site map: [/sitemap.md](https://www.highflame.com/sitemap.md). Machine index: [/sitemap-index.xml](https://www.highflame.com/sitemap-index.xml).
