Follow Discord
Sweep 25 Sep 2026 · 19:33Z Build v2.1.283 504 read Stable v2.1.274 Latest v2.1.283 Next v2.1.283 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · claude-docs

One read of Claude Documentationclaude-docs-20260925T180706Z

76 pages moved out of 255 read.

Pages moved 76 significant first
Pages read 255 in this capture
Captured 18:07 UTC
Corpus hash 62975fc51967 corpus-hash

What this read moved

1-25 of 76, page 1 of 4

This capture is too large to show at once. Changes 1-25 of 76 are below, significant first; the rest are on the following screens.

build/overview New page · 82 lines, new page

# Build for Claude ## Choose a starting page ## Steps to build and publish a plugin ### Tools that help you build and check a plugin ## Measure usage of what you built ## Next steps

A whole new page. There's nothing to diff it against, so here is what it says.

# Build for Claude

> Build a plugin people use inside Claude: an MCP connector to your product, skills for your workflows, optional MCP Apps UI, and a listing in the directory.

To bring your product or workflow into Claude, you build a [plugin](/docs/plugins/build): a package people add once and then use on claude.ai, in the desktop and mobile apps, in Cowork, and in Claude Code. A plugin packages whatever your integration needs, in any combination, and a plugin with only one of these pieces is complete:

* **[An MCP connector](/docs/connectors/building/index)**: include one when Claude needs to reach your product or data. It points at an MCP server that you build and host
* **[Skills](/docs/skills/how-to)**: include them to teach Claude your workflows, such as the sequence of steps, the defaults, and what good output looks like
* **[An MCP App](/docs/connectors/building/mcp-apps/getting-started)**: include one when you want the connector to show interactive UI in the conversation
* **Commands and agents**: include named actions, or specialists Claude can delegate to, which [Plugin structure and testing](/docs/plugins/build) covers

When the plugin is ready, you can [submit it](/docs/directory/publish) to Anthropic's directory at [claude.ai/directory](https://claude.ai/directory), the catalog inside Claude where people find and add plugins and connectors, so that anyone on a paid plan can add it. Anthropic checks each submission before it's listed, and there's no partner program to apply to first.

## Choose a starting page

<CardGroup cols={2}>
  <Card title="Build a plugin" icon="box" href="/docs/plugins/build" arrow>
    Lay out the plugin folder and manifest, write the skills, reference your MCP server, and test the plugin in Claude.
  </Card>

  <Card title="Write skills" icon="book-open" href="/docs/skills/how-to" arrow>
    Teach Claude your workflows: the SKILL.md frontmatter, the instructions, reference files, and scripts.
  </Card>

  <Card title="Build an MCP server" icon="server" href="/docs/connectors/building/index" arrow>
    Give the plugin access to your product or data: implement the server, set up authentication for Claude's clients, and test it.
  </Card>

  <Card title="Add interactive UI" icon="window" href="/docs/connectors/building/mcp-apps/getting-started" arrow>
    Have your MCP server show interactive components in the conversation with MCP Apps. Optional.
  </Card>
</CardGroup>

<Card title="Submit your plugin or MCP server" icon="store" href="/docs/directory/publish" horizontal arrow>
  List it in Anthropic's directory so anyone can add it: who can submit, what review involves, and the developer portal steps. Submit your MCP server as a connector too, even when your plugin references it.
</Card>

<Note>
  * If you want to connect a tool or install a plugin for yourself, see the [Customize Claude](/docs/extend/overview) tab
  * If you're not sure whether your plugin needs an MCP server at all, see [Decide what to include in your plugin](/docs/connectors/building/what-to-build)
  * If you're distributing only inside your organization, see [Roll out a plugin to your whole organization](/docs/plugins/org-rollout)
  * If you're building only for Claude Code's terminal and IDE users, see the [Claude Code plugin docs](https://code.claude.com/docs/en/plugins/create), which cover what that surface alone supports
</Note>

## Steps to build and publish a plugin

Building and publishing a plugin follows these steps in order:

1. **Lay out the plugin**: create the folder and manifest and reference any MCP server it uses. Start with [Build your first plugin](/docs/plugins/quickstart) if you haven't made one before, or [Plugin structure and testing](/docs/plugins/build) for the full layout. Chat, Cowork, and Claude Code each load different parts of a plugin, so check [Plugin feature support across platforms](/docs/plugins/platform-support) before you commit to a design.
2. **Write the skills**: teach Claude your workflows in `SKILL.md` files inside the plugin. [Create a skill](/docs/skills/how-to) covers the frontmatter, the instructions, reference files, and scripts
3. **Build the MCP server**: if the plugin needs to reach your product or data, implement the server, get authentication right for Claude's clients, and test it against Claude. Start with [Build an MCP server for Claude](/docs/connectors/building/index), or with [Build your first MCP server for Claude](/docs/connectors/building/quickstart) if you haven't written one before. A local server can also be packaged as a [desktop extension](/docs/connectors/building/mcpb).
4. **Add interactive UI**: if you want your server to show interactive components in the conversation, build them with [MCP Apps](/docs/connectors/building/mcp-apps/getting-started). This step is optional.
5. **Test it**: try the plugin in Claude before you publish it. [Test the plugin on each surface](/docs/plugins/build#test-the-plugin-on-each-surface) covers claude.ai, Cowork, and Claude Code, and [Test your MCP server](/docs/connectors/building/testing) covers adding your server as a custom connector
6. **Publish**: submit the plugin to the directory, submit its MCP server as a connector, and maintain the listing. Start with [Publish to the directory](/docs/directory/publish).
7. **Measure**: after people have it, [see whether they use it](#measure-usage-of-what-you-built).

### Tools that help you build and check a plugin

Most of the tooling for building and checking a plugin lives in [Claude Code](https://code.claude.com/docs/en/overview), Anthropic's command-line coding tool. You don't need it to use plugins, but you'll want it to build one: it validates your plugin folder, runs evals, and loads a plugin from a local folder so you can try it before you push. If you don't have it yet, [install Claude Code](https://code.claude.com/docs/en/setup) first. These are the tools to know:

* **[`claude plugin validate`](https://code.claude.com/docs/en/plugins/cli-reference#plugin-validate)**: run in your terminal to check the manifest and component files before you push
* **[`claude --plugin-dir`](https://code.claude.com/docs/en/plugins/create#load-a-directory-or-archive-for-one-session)**: start Claude Code with your plugin loaded from a local folder, so you can try its skills and commands before it's in a repository
* **[`claude plugin eval`](https://code.claude.com/docs/en/plugin-evals)**: run eval cases you write and compare Claude's results with and without your plugin. Each run is a real model call that counts against your plan's usage or your API bill
* **[`/skill-doctor`](https://code.claude.com/docs/en/skills#find-unused-skills)**: in a Claude Code session, see what each installed skill costs in context and how often Claude has invoked it
* **[`skill-creator`](/docs/skills/how-to#measure-whether-the-skill-improves-the-output)**: a skill from Anthropic that drafts a skill with you and runs it on test prompts; available in claude.ai and Claude Code
* **[`plugin-dev`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/plugin-dev)** and **[`mcp-server-dev`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev)**: plugins Anthropic publishes for Claude Code whose skills walk Claude through building a plugin or an MCP server with you

## Measure usage of what you built

Usage figures for a directory listing are in the developer portal, and figures for a plugin you distribute yourself or inside your organization are in Claude Code or in **Customize**. Each of these pages covers one case:

* **A plugin listed in the directory**: [How a published plugin is used](/docs/connectors/building/after-publishing#track-published-plugin-usage) covers installs, versions, how often each skill and MCP server runs, and error rates
* **A connector listed in the directory**: [Manage your directory listing](/docs/connectors/building/managing-your-listing#server-health-and-usage-metrics) covers the dashboard's server health, usage by product and by tool, and error breakdown
* **A plugin you rolled out to your organization's Claude Code users**: [Measure and evaluate plugins](https://code.claude.com/docs/en/plugins/measure) covers what a plugin costs in context, whether it's used, and the OpenTelemetry events that answer organization-wide questions
* **A plugin or skill used inside your organization on claude.ai**: [How a plugin is used in your organization](/docs/plugins/overview#track-plugin-usage-in-your-organization) covers the adoption and activity figures on its page in **Customize**

## Next steps

* [Build your first plugin](/docs/plugins/quickstart): build a small example plugin, test it, and push it to GitHub, ready to submit your own
* [Plugin structure and testing](/docs/plugins/build): the folder layout, manifest, skills, MCP server reference, and testing
* [Build an MCP server for Claude](/docs/connectors/building/index): implement the server, authentication, and testing against Claude
* [Decide what to include in your plugin](/docs/connectors/building/what-to-build): decide whether your plugin needs an MCP server, commands, agents, or UI

claude-science/changelog Changed · +10 / -0 lines

from line 2
22 
33> Release notes for Claude Science, including new features, improvements, and bug fixes by version.
44 
5<Update label="0.1.53" description="September 24, 2026">
6 * Paste a list of domains in **Settings > Network** to allow them all at once
7 * In Markdown files, editing table cells in place now works in files with up to 2,500 table cells, however long their text
8 * **Settings > Connectors**: a Featured connector that was turned off during setup now shows as off, and turning it back on lets Claude use it again
9 * Hover over a tab in the right pane to see the file's full name
10 * Mac: fixed a case where code cells didn't run because Claude wrote the folder's name in a different letter case or accent form than on disk
11 * Mac: security hardening of the analysis sandbox
12 * Various bug fixes and security improvements
13</Update>
14 
515<Update label="0.1.52" description="September 22, 2026">
616 * Clicking a figure in Claude's answer now opens it beside the chat; Ctrl/Cmd-click or the new "Open fullscreen" button opens it full screen
717 * Opening or closing the right pane now keeps your place in a long answer

claude-science/core-concepts Changed · +8 / -8 lines

from line 22
2222 
2323A permission card appears in the conversation each time Claude needs a new kind of access. The card names exactly what's being requested. You can allow or deny each one.
2424 
25| Action | Card title | Scope options |
26| ---------------------- | ----------------------------------------------------------- | ------------------------------------------------- |
27| Read or write a folder | Access `<folder>` on your computer? | Read-only or Read & write; persists until revoked |
28| Run code | Run Python code? / Run a shell command? / Install packages? | Once, This conversation, This project, or Global |
29| Reach a network host | Connect to `<target>`? | Persists until revoked |
30| Use a connector tool | Use `<tool>`? | Once, This conversation, This project, or Global |
31| Use a saved credential | Credentials | Once, This conversation, This project, or Global |
32| Run a remote job | Run this job on `<host>`? / Start a Modal job? | Once, This conversation, This project, or Global |
25| Action | Card title | Scope options |
26| ---------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
27| Read or write a folder | Access `<folder>` on your computer? | Read-only or Read & write; persists until revoked |
28| Run code | Run Python code? / Run R code? / Run a shell command? / Run a PowerShell command? (Windows) / Install packages? | Once, This conversation, This project, or Global |
29| Reach a network host | Connect to `<target>`? | Persists until revoked |
30| Use a connector tool | Use `<tool>`? | Once, This conversation, This project, or Global |
31| Use a saved credential | Credentials | Once, This conversation, This project, or Global |
32| Run a remote job | Run this job on `<host>`? / Start a Modal job? | Once, This conversation, This project, or Global |
3333 
3434All standing grants are listed in Settings > Permissions and can be revoked there.
3535 

connectors/building/after-publishing Changed · +54 / -11 lines

## Update a published connector or plugin ### MCP server changes ### Plugin changes ### Listing details ## Track published plugin usage ## Directory listing URLs are permanent ## Delist a connector or plugin ## Next steps ## Update your MCP server ## Update your plugin ## Update your listing ## Slugs are permanent ## Delist your connector

from line 1
11# Manage your listing after publishing
22 
3> Update your MCP server, plugin, and directory listing after publication
3> Update your MCP server, plugin, and directory listing after publication, keep your listing URL stable, and delist when you need to.
44 
5## Update your MCP server
5Once your connector or plugin is listed in the directory, you keep it current from the same developer portal you submitted from, at [claude.ai/directory/manage](https://claude.ai/directory/manage).
66 
7An MCP server is a live API. To add, change, or remove tools, deploy the change to your server—no resubmission to Anthropic is required, and there is no scheduled re-review. Claude picks up the new tool surface on the next connection.
7This page is for the developer who owns a published listing.
88 
9## Update your plugin
9<Note>
10 If your submission isn't live yet, see [Track your directory submission](/docs/directory/submission-status), which explains what each status in the portal means and who acts next.
11</Note>
1012 
11Plugin updates are pushed via your GitHub repo. CI mirrors changes to the public marketplace and runs automated screening on each update.
13You can [update what you've published](#update-a-published-connector-or-plugin), [track how a published plugin is used](#track-published-plugin-usage), [share your listing's permanent URL](#directory-listing-urls-are-permanent), or [delist](#delist-a-connector-or-plugin).
1214 
13## Update your listing
15## Update a published connector or plugin
1416 
15Edit your description, categories, icon, and other listing metadata from the submissions dashboard at [Organization settings > Directory](https://claude.ai/admin-settings/directory/submissions) in Claude.ai. See [Managing your listing](/docs/connectors/building/managing-your-listing) for what you can edit directly and which changes require review.
17You don't resubmit to Anthropic to change a listed MCP server's tools or a listed plugin's files.
1618 
17## Slugs are permanent
19### MCP server changes
1820 
21To add, change, or remove tools, deploy the change to your server. The tool names shown on your directory listing are part of the listing details, so update them with a [listing edit](/docs/connectors/building/managing-your-listing#edit-your-listing), which a reviewer approves.
22 
23### Plugin changes
24 
25To update a listed plugin, push to the branch or tag the directory tracks for it. The directory picks up the new commit, runs the same validation and security scan as your first submission, and shows the result as a new version on the plugin's page in the portal. If you set `version` in `plugin.json`, raise it with each release, because installed copies use it to tell that an update exists.
26 
27A version that doesn't pass, or that is held for a reviewer, doesn't take your listing down: the directory keeps serving the last published version until a newer one is published. [Submit a plugin](/docs/plugins/submit#update-a-published-plugin) covers how the directory finds new commits, how to have GitHub notify it on push, and how to change the tracked branch or tag.
28 
29### Listing details
30 
31For a connector, edit your description, categories, icon, and other listing details from the developer portal at [claude.ai/directory/manage](https://claude.ai/directory/manage). See [Manage your directory listing](/docs/connectors/building/managing-your-listing) for what you can edit directly and which changes require review.
32 
33A plugin listing's name and short description come from `plugin.json` and the README of the version that's live. To change them, edit those files and publish a new version, as [Plugin changes](#plugin-changes) describes. If an Anthropic reviewer edited either field during review, the listing keeps the reviewer's text. From the plugin's **Settings** tab in the developer portal, you can change which surfaces the plugin is listed on, your contact email, and your data handling answers.
34 
35## Track published plugin usage
36 
37A published plugin's page in the developer portal has a **Usage** tab. The portal is open to the people who [can submit](/docs/directory/publish#confirm-you-can-submit-to-the-directory). Use it to see how many accounts install and use the plugin, which version they're on, and whether the plugin loads and its MCP servers respond without errors.
38 
39The **Usage** tab covers a period you choose, up to 90 days, and you can export the figures as CSV. It shows these groups of figures:
40 
41* **Reach**: installs, active accounts, and retention, with installs broken out by surface and by where the install came from, such as the **Discover** tab
42* **Versions**: the share of accounts on each version
43* **Components**: how often each skill, command, agent, hook, and MCP server is used
44* **Quality**: load errors by Claude Code version, and tool calls, error rate, and latency for each MCP server
45* **Directory funnel**: listing views, install clicks, and installs
46 
47Figures are computed once a day in UTC, and the tab shows the date the data runs through. Until usage is recorded for your plugin, the tab is marked **Preview** and shows sample numbers.
48 
49For a connector, [Manage your directory listing](/docs/connectors/building/managing-your-listing#server-health-and-usage-metrics) covers the health badge and usage metrics.
50 
51## Directory listing URLs are permanent
52 
1953Your directory slug is fixed after publication. It determines your connector's permanent listing URL:
2054 
2155```text theme={null}
from line 56
2256https://claude.ai/directory/connectors/SLUG
2357```
2458 
25Share this URL from your own documentation or a "Connect to Claude" button to send users directly to your listing. Display names can change via the dashboard; the URL slug cannot.
59Share the listing URL from your own documentation or a **Connect to Claude** button to send users directly to your listing. You can change display names via the dashboard, but you can't change the URL slug.
2660 
27## Delist your connector
61## Delist a connector or plugin
2862 
29To voluntarily remove your connector from the directory, email `[email protected]`.
63You delist a plugin from the developer portal and a connector by email:
64 
65* **To remove a plugin listing:** open the plugin in the developer portal and select **Delist plugin** from the plugin's menu or its **Settings** tab
66* **To bring a plugin listing back:** select **Relist plugin** on the **Settings** tab, which asks the directory to restore the listing
67* **To remove a connector from the directory:** email `[email protected]`
68 
69## Next steps
70 
71* [Manage your directory listing](/docs/connectors/building/managing-your-listing): for a connector, check submission status and health and usage metrics, and edit the listing
72* [Connector verification](/docs/connectors/verification#list-your-own-connector): see how a listing becomes Verified and what each label means to people installing it
3073 

connectors/building/authentication Changed · +169 / -130 lines

### Static credentials in request headers ### Servers with per-customer URLs ## Register Claude as an OAuth client ### DCR and CIMD details ### Anthropic-held client credentials ### Credentials entered at connection time ### PKCE and requested scopes ## OAuth discovery and redirect URIs ### Serve discovery metadata ### Callback URLs ## Token endpoint requirements ### Token refresh ### Endpoint latency ## Enterprise and custom connector authentication ### Enterprise authentication ### Custom connectors ## Next steps ## Servers with per-customer URLs ## Anthropic-held client credentials ## Credentials entered at connection time ## DCR and CIMD details ## Cross-host authorization servers ## Callback URLs ## Token refresh ## Enterprise authentication ## Custom connectors ## Endpoint latency

from line 1
11# Authentication for connectors
22 
3> OAuth and authentication options for MCP servers in Claude
3> Meet Claude's OAuth requirements for remote MCP servers: supported authentication types, client registration, discovery, callback URLs, and token refresh.
44 
5Authentication is the most common source of partner questions. Claude's auth support differs in a few places from the generic MCP specification, so read this page even if you're already familiar with MCP auth.
5A remote MCP server can let Claude in one of three ways:
66 
7* **OAuth 2.0**: each user signs in to your service with their own account when they connect
8* **A static credential**: an organization Owner enters an API key or bearer token once when adding the connector, and Claude sends it in a request header on every call. This is in beta
9* **No authentication**: the server accepts requests from anyone who has its URL
10 
11This page is for developers building a remote MCP server that people use in Claude. The same authentication infrastructure backs claude.ai, Claude Desktop, Claude mobile, Claude Code, and Cowork, so the requirements here apply to all of them.
12 
13If you already know MCP authorization, these are the places where Claude's OAuth client is stricter or more specific than the specification:
14 
15* A `401` is required to start sign-in, and Claude ignores a `WWW-Authenticate` header on a `200` response, as [Serve discovery metadata](#serve-discovery-metadata) describes
16* Claude uses only the first entry in your metadata's `authorization_servers` list, as [Serve discovery metadata](#serve-discovery-metadata) describes
17* Claude uses a Client ID Metadata Document only when your authorization server metadata advertises both values in [DCR and CIMD details](#dcr-and-cimd-details), and otherwise falls back to DCR
18* Claude Code's loopback redirect needs a port-agnostic match for `localhost` as well as `127.0.0.1`, as [Callback URLs](#callback-urls) describes
19* Claude gives your discovery, registration, and token endpoints 10 seconds to respond and refresh requests 30 seconds, as [Endpoint latency](#endpoint-latency) describes
20* A machine-to-machine `client_credentials` grant isn't supported, and [Anthropic-held client credentials](#anthropic-held-client-credentials) are the consent-gated alternative
21 
22<Note>
23 - If some of your tools work without the user's account, see [Lazy authentication](/docs/connectors/building/lazy-authentication) to let people use those right away and sign in only when Claude reaches a tool that needs their account
24 - If you want enterprise users to connect through their organization's SSO without a consent screen, see [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth)
25</Note>
26 
27Use this page to [pick an authentication type](#supported-authentication-types), [register Claude as an OAuth client](#register-claude-as-an-oauth-client), [make discovery and redirects work](#oauth-discovery-and-redirect-uris), and [meet the token endpoint requirements](#token-endpoint-requirements).
28 
729## Supported authentication types
830 
9Claude supports the following authentication types for remote MCP servers. The same infrastructure backs Claude.ai, Claude Desktop, Claude mobile, Claude Code, and Cowork.
31Claude supports the following authentication types for remote MCP servers.
1032 
11| Type | Description | Availability |
12| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
13| `oauth_dcr` | OAuth 2.0 with Dynamic Client Registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) | Supported out of the box |
14| `oauth_cimd` | OAuth 2.0 with [Client ID Metadata Document](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents) | Supported out of the box |
15| `oauth_anthropic_creds` | OAuth 2.0 with [Anthropic-held client credentials](#anthropic-held-client-credentials) | Contact `[email protected]` |
16| `custom_connection` | Custom URL or OAuth client credentials [entered at connection time](#credentials-entered-at-connection-time) | Contact `[email protected]` |
17| `static_headers` | Fixed credential (API key or bearer token) entered by an organization administrator as a request header when adding the connector | Beta |
18| `none` | No authentication (authless server) | Supported. An optional partial-auth mode is experimental. |
33| Type | Description | Availability |
34| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
35| `oauth_dcr` | OAuth 2.0 with Dynamic Client Registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) | Supported by default |
36| `oauth_cimd` | OAuth 2.0 with [Client ID Metadata Document](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents) | Supported by default |
37| `oauth_anthropic_creds` | OAuth 2.0 with [Anthropic-held client credentials](#anthropic-held-client-credentials) | Contact `[email protected]` |
38| `custom_connection` | Custom URL or OAuth client credentials [entered at connection time](#credentials-entered-at-connection-time) | Contact `[email protected]` |
39| `static_headers` | Fixed credential (API key or bearer token) entered by an organization Owner as a request header when adding the connector | Beta, for a limited set of organizations |
40| `none` | No authentication (authless server) | Supported by default. To leave some tools open and require sign-in for others, see [Lazy authentication](/docs/connectors/building/lazy-authentication) |
1941 
2042If your server URL varies per customer, read [Servers with per-customer URLs](#servers-with-per-customer-urls) before you pick a type.
2143 
22Static bearer tokens and API keys are supported in beta through request headers (`static_headers`). An organization administrator enters the credential once when adding the connector, and Claude sends it on every request. The credential is shared by the organization rather than pasted per user. Standard header names such as `authorization` and `x-api-key` work for every connector; Anthropic reviews and approves any other header name before administrators can save the connector. See [Authenticating with request headers](/docs/connectors/custom/remote-mcp#authenticating-with-request-headers) for what administrators see and how to document the expected header for them.
44### Static credentials in request headers
2345 
24Tokens or API keys passed in the connector URL (for example, `?token=`, `?apiKey=`, or `?userToken=` query parameters) are **not recommended**. A credential in a URL is a security vulnerability: URLs are routinely recorded in server logs, proxies, and browsing history, so a query-string credential is easy to leak. The MCP authorization specification explicitly [prohibits access tokens in the URI query string](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-requirements). Use OAuth or [request headers](/docs/connectors/custom/remote-mcp#authenticating-with-request-headers) instead.
46With a static credential, an organization Owner enters an API key or bearer token once when they add your connector, and Claude sends it in a request header on every call from anyone in that organization. This type is in beta and available to a limited set of organizations. Owners whose organization doesn't have access don't see the **Request headers** section when they add a connector. If your server uses it:
2547 
26## Servers with per-customer URLs
48* **Read the credential from a request header**: standard authentication header names such as `authorization`, `x-api-key`, and `x-auth-token` work for every connector. If you need a different header name, Anthropic has to approve it before Owners can save the connector, so ask `[email protected]` first
49* **Never accept it in the URL**: don't read tokens from query parameters such as `?token=` or `?apiKey=`. URLs end up in server logs, proxies, and browser history, and the MCP authorization specification [prohibits access tokens in the query string](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-requirements)
50* **Treat it as the organization's credential, not a person's**: every member's requests carry the same key, so don't use it to identify which user is calling. If your tools need to act as the individual user, use OAuth instead
51* **Tell Owners what to enter**: document the header name and where they get the key. [Authenticate with request headers](/docs/connectors/custom/add-unlisted#authenticate-with-request-headers) shows what the Owner sees when adding the connector
2752 
28The submission portal's **Connection** step asks how users reach your server. There are three choices:
53### Servers with per-customer URLs
2954 
30* **Universal URL**: every user connects to the same URL.
31* **Multiple URLs**: you list a fixed set of labeled URLs, such as one per region. Users pick one when they connect.
32* **URL pattern**: you give an anchored regular expression that every customer's URL must match, such as `^https://[a-z0-9-]+\.mcp\.example\.com/mcp$`. Each user enters their own URL when they connect, and Claude accepts it only if it matches.
55The submission portal's **Connection** step asks how users reach your server, and you pick one of these options:
3356 
34Keep the host part of a URL pattern lowercase, because Claude lowercases the host of the URL the user enters before checking it.
57* **Universal URL**: every user connects to the same URL
58* **Multiple URLs**: you list a fixed set of labeled URLs, such as one per region, and users pick one when they connect
59* **URL pattern**: you give an anchored regular expression that every customer's URL must match, such as `^https://[a-z0-9-]+\.mcp\.example\.com/mcp$`. Each user enters their own URL when they connect, and Claude accepts it only if it matches. Keep the host part of the pattern lowercase, because Claude lowercases the host of the URL the user enters before checking it
3560 
3661Listings with **Multiple URLs** or a **URL pattern** take longer to review.
3762 
38You choose the URL option and the authentication type separately, but the URL option limits which authentication types work. Request headers (`static_headers`) are set up by the organization administrator who adds the connector and aren't covered by this table.
63You choose the URL option and the authentication type separately, but the URL option limits which authentication types work. The table shows which combinations work. Request headers (`static_headers`) are set up by the organization Owner who adds the connector and aren't covered here.
3964 
4065| Type | Universal URL | Multiple URLs | URL pattern |
4166| ----------------------- | ------------- | ------------- | ----------- |
from line 70
4570| `custom_connection` | Yes | No | Yes |
4671| `none` | Yes | Yes | Yes |
4772 
48Anthropic-held client credentials are tied to exact server URLs, and a URL pattern matches URLs Anthropic doesn't know in advance. Credentials entered at connection time can't be combined with **Multiple URLs**.
73For a URL pattern, use these authentication types in order of preference:
4974 
50For a URL pattern, use these in order of preference:
51 
52751. Client ID Metadata Document (CIMD). Every customer's authorization server must advertise both CIMD values listed in [DCR and CIMD details](#dcr-and-cimd-details).
53762. [Dynamic Client Registration](#dcr-and-cimd-details) (DCR). Every customer's authorization server must expose a `registration_endpoint`.
54773. [Credentials entered at connection time](#credentials-entered-at-connection-time), if your customers' authorization servers support neither. Each customer then has to create an OAuth client for Claude themselves.
5578 
56## Anthropic-held client credentials
79## Register Claude as an OAuth client
5780 
58A pure machine-to-machine `client_credentials` grant—where a server-to-server token is issued with no user in the loop—is **not supported**. Every connection requires user consent.
81For the OAuth types, Claude needs a client identity at your authorization server. Claude can register itself through DCR or identify itself with a CIMD, Anthropic can hold a client you create, or each customer can enter their own client when they connect.
5982 
60`oauth_anthropic_creds` is the consent-gated alternative. The flow works like this:
83### DCR and CIMD details
6184 
621. You create an OAuth `client_id` and `client_secret` in your own authorization server and send them to Anthropic.
632. Anthropic stores those credentials securely and associates them with your directory entry.
643. When a user connects your server, they go through a standard OAuth consent screen.
654. After consent, Anthropic uses the stored client credentials to complete the token exchange on the user's behalf.
85If your authorization server doesn't support DCR, meaning it exposes no `registration_endpoint`, you have these options:
6686 
67This gives you a stable, registered OAuth client without requiring DCR or CIMD on your end, while keeping the user-consent step. Anthropic stores your credentials securely and uses them only for token exchange on behalf of consenting users; they are shared across the hosted Claude surfaces (Claude.ai web, Desktop, mobile, and Cowork). Claude Code runs its own OAuth flow on the user's machine and identifies itself with its own [Client ID Metadata Document](#callback-urls), so it does not use Anthropic-held credentials. Claude Managed Agents uses a separate credential set.
87* Expose a `registration_endpoint`
88* Support CIMD instead. Claude selects CIMD only when your authorization server metadata advertises both `"client_id_metadata_document_supported": true` and `"none"` in `token_endpoint_auth_methods_supported`. The second is required because Claude's CIMD client authenticates as a public client at your token endpoint. If either is missing, Claude falls back to DCR. See [lazy authentication](/docs/connectors/building/lazy-authentication#identify-claude-with-a-client-id-metadata-document) for a worked CIMD example
89* Switch to `oauth_anthropic_creds`, if your listing doesn't use a URL pattern
6890 
69Anthropic-held credentials are bound to the authorization server that issued them. If you migrate to a new authorization server, email `[email protected]` with the new `client_id` and `client_secret` before cutting over. CIMD-based connectors don't have this constraint — a CIMD `client_id` is a self-hosted URL, so it works against any authorization server that fetches it.
91If your server URL varies per customer and DCR isn't available, CIMD is the recommended path. Every customer's authorization server must advertise both CIMD values. Otherwise Claude falls back to DCR for that customer, which needs a `registration_endpoint`.
7092 
71<Note>
72 Anthropic-held credentials are also tied to exact server URLs. They can't be used with a URL pattern, where each customer enters their own server URL. See [Servers with per-customer URLs](#servers-with-per-customer-urls) for the alternatives.
73</Note>
93For servers expecting high traffic from the directory, prefer CIMD or `oauth_anthropic_creds` over DCR. DCR causes Claude to register a new client on every fresh connection, which can result in very large numbers of registered clients on your authorization server. CIMD and Anthropic-held credentials avoid the registration call entirely.
7494 
75To use this flow, email `[email protected]` with your `client_id` and secret.
95### Anthropic-held client credentials
7696 
77## Credentials entered at connection time
97With `oauth_anthropic_creds`, you create an OAuth client for Claude in your own authorization server and Anthropic holds its credentials, so Claude has a stable, registered client without DCR or CIMD on your end. Users still go through your standard OAuth consent screen when they connect. A pure machine-to-machine `client_credentials` grant, where a token is issued with no user in the loop, isn't supported.
7898 
79`custom_connection` (**Custom URL or credentials at connection time** in the submission portal) asks each customer for the OAuth client that Claude should use, instead of Claude registering one or Anthropic holding one.
99To use this type:
80100 
101* **Create a confidential client**: create a `client_id` and `client_secret` for Claude in your authorization server
102* **Send the credentials to Anthropic**: email `[email protected]` with the `client_id` to set up Anthropic-held credentials. Anthropic replies with how to transfer the client secret securely, so don't put the secret in the email. Anthropic uses the credentials only for token exchange on behalf of consenting users
103* **Plan for Claude Code separately**: the hosted Claude apps, which are claude.ai on the web, Desktop, mobile, and Cowork, share this client. Claude Code doesn't use it: it runs its own OAuth flow on the user's machine, identifies itself with its own [Client ID Metadata Document](#callback-urls), and redirects to a loopback callback URL
104* **Tell Anthropic before you migrate authorization servers**: the credentials are bound to the authorization server that issued them. Email `[email protected]` with the new `client_id` before cutting over, and transfer the new secret the same way as the first
105* **Don't combine it with a URL pattern**: the credentials are tied to exact server URLs, so they can't be used where each customer enters their own server URL. See [Servers with per-customer URLs](#servers-with-per-customer-urls) for the alternatives
106 
107### Credentials entered at connection time
108 
109`custom_connection`, labeled **Custom URL or credentials at connection time** in the submission portal, asks each customer for the OAuth client that Claude should use, instead of Claude registering one or Anthropic holding one.
110 
81111Each customer must be able to create an OAuth client in your product, which usually means an administrator sets up the connector for their organization. If your customers can't create OAuth clients, use CIMD or DCR instead.
82112 
83113When a user adds your connector, Claude shows a form with these fields:
84114 
85* **Server URL**, if your listing uses a URL pattern. Claude accepts the URL only if it matches the pattern.
86* **OAuth client ID** and **OAuth client secret**. You choose which of the two to ask for, and whether each is required or optional.
115* **Server URL**, if your listing uses a URL pattern. Claude accepts the URL only if it matches the pattern
116* **OAuth client ID** and **OAuth client secret**. You choose which of the two to ask for, and whether each is required or optional
87117 
88118The form links to pages you supply: one for where the customer finds their server URL, and one for how they get the credentials. The credentials page must explain how a customer creates an OAuth client for Claude in your product and registers the redirect URI `https://claude.ai/api/mcp/auth_callback`.
89119 
90If a user leaves an optional client secret blank, Claude uses the client ID as a public client. If a user leaves an optional client ID blank, Claude falls back to the same order as any other listing: Anthropic-held credentials for that URL if Anthropic holds any, then CIMD, then DCR. See [DCR and CIMD details](#dcr-and-cimd-details).
120If a user leaves an optional client secret blank, Claude uses the client ID as a public client. If a user leaves an optional client ID blank, Claude falls back to its standard order: Anthropic-held credentials for that URL if Anthropic holds any, then CIMD, then DCR. See [DCR and CIMD details](#dcr-and-cimd-details).
91121 
92If you later stop asking for credentials, connections already made with user-entered credentials keep using them. An organization gets the new behavior only once no one in it still has the connector. The next person to add it starts fresh.
122If you later stop asking for credentials, connections already made with user-entered credentials keep using them, and an organization keeps the form until no one in it still has the connector.
93123 
94124To use this flow, email `[email protected]` with which fields you need, whether each is required, and the page each one should link to.
95125 
96## DCR and CIMD details
126### PKCE and requested scopes
97127 
98If your authorization server does **not** expose a `registration_endpoint` (i.e., does not support DCR), you have several options:
128Claude includes a [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) `code_challenge` with `code_challenge_method=S256` on every authorization request, regardless of which registration mechanism it uses. Your authorization server must support S256 PKCE. The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-code-protection) also requires it to advertise `"code_challenge_methods_supported": ["S256"]` in its metadata so spec-compliant clients can verify support before starting the flow.
99129 
100* Expose a `registration_endpoint`
101* Support CIMD instead. Claude selects CIMD only when your authorization server metadata advertises **both** `"client_id_metadata_document_supported": true` **and** `"none"` in `token_endpoint_auth_methods_supported` — the second is required because Claude's CIMD client authenticates as a public client at your token endpoint. If either is missing, Claude falls back to DCR. See [lazy authentication](/docs/connectors/building/lazy-authentication#identify-the-client-with-cimd) for a worked CIMD example.
102* Switch to `oauth_anthropic_creds`, if your listing doesn't use a URL pattern
130To control which scopes Claude requests, include a `scope` parameter in the `WWW-Authenticate` header on your `401` response. If you don't, Claude requests the scopes your protected resource metadata advertises in `scopes_supported`. Claude also appends `offline_access` when your authorization server metadata lists it in `scopes_supported`, to obtain a refresh token. See [lazy authentication](/docs/connectors/building/lazy-authentication#answer-a-protected-call-with-401-before-the-mcp-sdk-runs) for the canonical `401` shape.
103131 
104If your server URL varies per customer and DCR isn't available, CIMD is the recommended path. Every customer's authorization server must advertise both values above. Otherwise Claude falls back to DCR for that customer, which needs a `registration_endpoint`.
132## OAuth discovery and redirect URIs
105133 
106For servers expecting high traffic from the directory, prefer **CIMD or `oauth_anthropic_creds` over DCR**. DCR causes Claude to register a new client on every fresh connection, which can result in very large numbers of registered clients on your authorization server. CIMD and Anthropic-held credentials avoid the registration call entirely.
134Claude finds your authorization server by reading your protected resource metadata, then sends the user back to a redirect URI that depends on which Claude surface they're using. Both steps have requirements your server and authorization server must meet.
107135 
108Claude includes a [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) `code_challenge` with `code_challenge_method=S256` on every authorization request, regardless of which registration mechanism it uses. Your authorization server must support S256 PKCE, and the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-code-protection) requires it to advertise `"code_challenge_methods_supported": ["S256"]` in its metadata so spec-compliant clients can verify support before starting the flow.
136### Serve discovery metadata
109137 
110To control which scopes Claude requests, include a `scope` parameter in the `WWW-Authenticate` header on your `401` response. If you don't, Claude requests the scopes your protected resource metadata advertises in `scopes_supported`. Claude also appends `offline_access` when your authorization server metadata lists it in `scopes_supported`, to obtain a refresh token. See [lazy authentication](/docs/connectors/building/lazy-authentication#return-401-not-a-tool-error) for the canonical `401` shape.
138Claude locates your authorization server through your [protected resource metadata](https://www.rfc-editor.org/rfc/rfc9728) document, and the authorization server it names can be on a different host from your MCP server. For Claude to find and use that document:
111139 
112## Cross-host authorization servers
140* **Return `401` with a `resource_metadata` pointer**: answer unauthenticated requests with a `401` whose `WWW-Authenticate` header points at the document
141* **Make `resource` match your MCP server URL exactly**: the document's `resource` field must equal the URL as the user enters it in Claude, including any path component
142* **List your primary issuer first**: the document's `authorization_servers` field must list your authorization server's issuer URL. If you list more than one, Claude uses the first entry and doesn't fall back to later entries
143* **Serve authorization server metadata Claude can reach**: your authorization server must serve its own discovery metadata, either [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) authorization server metadata or [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html), at its `/.well-known/` paths. That host must also be reachable from Anthropic's [published egress range](https://platform.claude.com/docs/en/api/ip-addresses). Discovery requests to the authorization server come from the same IP range as requests to your MCP server, so a WAF in front of your identity provider can break the flow even when your MCP server is reachable
113144 
114A cross-host authorization server doesn't need anything special on its own. The `authorization_servers` field in your [protected resource metadata](https://www.rfc-editor.org/rfc/rfc9728) tells Claude where the authorization server is, and Claude resolves it regardless of which host it points at. The thing to get right is making sure Claude can find the protected resource metadata in the first place.
145The `401` response carries the pointer in its `WWW-Authenticate` header. This is the same handshake described in [Answer a protected call with 401 before the MCP SDK runs](/docs/connectors/building/lazy-authentication#answer-a-protected-call-with-401-before-the-mcp-sdk-runs):
115146 
116**Always return a `401` with a `WWW-Authenticate` header** whose `resource_metadata` parameter points at your protected resource metadata document — the same handshake described in [Return 401, not a tool error](/docs/connectors/building/lazy-authentication#return-401-not-a-tool-error):
117 
118147```http theme={null}
119148HTTP/1.1 401 Unauthorized
120149WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
121150```
122151 
123The `401` status is required — Claude does not honor a `WWW-Authenticate` header on a `200` response — and the `resource_metadata` URL doesn't have to be on the MCP server's origin; it can be any HTTPS location that serves the JSON document. That's what makes this the most reliable path for hosting platforms that can't serve `/.well-known/*` at the root, such as Supabase Edge Functions, Cloudflare Workers without a `/.well-known/*` route, and Lambda function URLs that only route a path prefix.
152The `401` status is required, because Claude doesn't honor a `WWW-Authenticate` header on a `200` response. The `resource_metadata` URL doesn't have to be on the MCP server's origin. It can be any HTTPS location that serves the JSON document, which makes this the most reliable path on serverless or edge platforms that only route requests under a path prefix and can't serve `/.well-known/*` at the root.
124153 
125If your `401` doesn't include a `resource_metadata` pointer, Claude can still infer the metadata location by probing your MCP server's origin: `/.well-known/oauth-protected-resource/<your-mcp-path>` first, then `/.well-known/oauth-protected-resource`. Treat this as a fallback — it only works when your platform serves `/.well-known/*` paths, and it adds round-trips to every connection.
154If your `401` doesn't include a `resource_metadata` pointer, Claude can still infer the metadata location by probing your MCP server's origin: `/.well-known/oauth-protected-resource/<your-mcp-path>` first, then `/.well-known/oauth-protected-resource`. Treat this as a fallback, because it works only when your platform serves `/.well-known/*` paths.
126155 
127Whichever way Claude finds the document:
128 
129* The protected resource metadata document's `resource` field must match your MCP server URL exactly as the user enters it in Claude, including any path component.
130* The metadata's `authorization_servers` field must list your authorization server's issuer URL. If you list more than one, Claude uses the first entry and does not fall back to later entries — list your primary issuer first.
131* Your authorization server must serve its own discovery metadata — [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) authorization server metadata or [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html) — at its `/.well-known/` paths, and that host must also be reachable from Anthropic's [published egress range](https://platform.claude.com/docs/en/api/ip-addresses). Discovery requests to the authorization server come from the same IP range as requests to your MCP server, so a WAF in front of your identity provider can break the flow even when your MCP server is reachable.
132 
133156<Note>
134 If your authorization server is Microsoft Entra ID, you must also register the MCP server URL as an Application ID URI on your Entra app registration, or the token request fails with `AADSTS9010010`. By default, Entra accepts that URL as an Application ID URI only when it's on a domain your tenant has verified (see [Microsoft's identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions)), so an MCP server on a platform hostname such as `*.azurewebsites.net` needs a custom domain first. See [the troubleshooting entry](/docs/connectors/building/troubleshooting#microsoft-entra-id-rejects-the-resource-value) for the fix.
157 If your authorization server is Microsoft Entra ID, you must also register the MCP server URL as an Application ID URI on your Entra app registration, or the token request fails with `AADSTS9010010`. By default, Entra accepts that URL as an Application ID URI only when it's on a domain your tenant has verified, as described in [Microsoft's identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions), so an MCP server on a platform hostname such as `*.azurewebsites.net` needs a custom domain first. See [the troubleshooting entry](/docs/connectors/building/troubleshooting#microsoft-entra-id-rejects-the-resource-value) for the fix.
135158</Note>
136159 
137160If you control both hosts, an alternative is to serve the MCP endpoint and the authorization server behind a single custom domain that can route both `/.well-known/*` and your MCP path.
from line 163
140163 A common symptom of a discovery failure is that your MCP server receives the initial request but your authorization server sees no traffic at all. That happens when neither path works: there's no `WWW-Authenticate: Bearer resource_metadata=…` header on your `401`, and the well-known paths on your MCP server's origin return `404`. With no metadata to read, Claude never learns where your authorization server is, and the connection fails with "Couldn't reach the MCP server." See [troubleshooting](/docs/connectors/building/troubleshooting) for the full diagnostic flow.
141164</Tip>
142165 
143## Callback URLs
166### Callback URLs
144167 
145For the hosted Claude surfaces (Claude.ai web, Desktop, mobile, and Cowork), register the following redirect URI:
168The redirect URI Claude sends depends on which surface the user connects from: the hosted Claude apps use one fixed callback URL and Claude Code uses a loopback redirect. Your authorization server must accept both.
146169 
147```
170For the hosted Claude apps, which are claude.ai on the web, Desktop, mobile, and Cowork, register exactly this redirect URI:
171 
172```text theme={null}
148173https://claude.ai/api/mcp/auth_callback
149174```
150175 
151**Claude Code** is a native client and uses an RFC 8252 loopback redirect on an ephemeral port — for example:
176For Claude Code, accept a loopback redirect on any port. Claude Code is a native client and uses an [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252) loopback redirect on an ephemeral port that varies per session, such as:
152177 
153```
178```text theme={null}
154179http://localhost:3118/callback
155180```
156181 
157The port varies per session. Claude Code declares `http://localhost/callback` and `http://127.0.0.1/callback` in its [Client ID Metadata Document](https://claude.ai/oauth/claude-code-client-metadata), so your authorization server must accept both with the port component ignored. [RFC 8252 section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3) requires this for the IP-literal form (`127.0.0.1`); apply the same port-agnostic match to `localhost` so Claude Code works, even though RFC 8252 section 8.3 discourages `localhost`. See [lazy authentication](/docs/connectors/building/lazy-authentication) for implementation details.
182Claude Code declares `http://localhost/callback` and `http://127.0.0.1/callback` in its [Client ID Metadata Document](https://claude.ai/oauth/claude-code-client-metadata), so match both with the port component ignored. [RFC 8252 section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3) requires this for the IP-literal form (`127.0.0.1`). Apply the same port-agnostic match to `localhost` so Claude Code works, even though RFC 8252 section 8.3 discourages `localhost`. See [lazy authentication](/docs/connectors/building/lazy-authentication#match-loopback-redirect-uris-without-the-port) for implementation details.
158183 
159A Client ID Metadata Document can't prevent loopback impersonation on its own — any local process can bind a port and claim to be the legitimate client. The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#localhost-redirect-uri-risks) requires authorization servers to display the redirect URI hostname clearly on the consent screen and recommends an extra warning when the only registered redirect URIs are loopback addresses.
184On your consent screen, display the redirect URI's hostname clearly. The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#localhost-redirect-uri-risks) requires this and recommends an extra warning when the only registered redirect URIs are loopback addresses, because any local process can bind a port and claim to be the client.
160185 
161## Token refresh
186## Token endpoint requirements
162187 
163Claude refreshes tokens **reactively on a 401 response**, with a proactive refresh up to five minutes before the stored expiry. To avoid refresh failures:
188Your token endpoint handles Claude's initial code exchange and every later refresh, and Claude enforces response-time limits on it and on your other OAuth endpoints.
164189 
165* Return RFC 6749-compliant error codes (`invalid_grant`, not `invalid_request` or a custom code) when a refresh token is no longer valid
166* Rotate refresh tokens for public-client connections. DCR and CIMD register Claude as a public client, and the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-theft) adopts OAuth 2.1's requirement to rotate or sender-constrain refresh tokens for public clients. If you rotate, return the new refresh token in the same response that invalidates the old one.
190### Token refresh
167191 
168Your `/token` endpoint must accept `Content-Type: application/x-www-form-urlencoded` per [RFC 6749 section 4.1.3](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3). Claude sends both the initial token exchange and refresh requests with this content type. Some web frameworks default to JSON-only body parsing—if your endpoint returns `415 Unsupported Media Type`, register a form-urlencoded body parser. Dynamic client registration (`/register`) uses `application/json` per [RFC 7591 section 3.1](https://www.rfc-editor.org/rfc/rfc7591#section-3.1), so don't assume the same parser works for both.
192Claude refreshes tokens reactively on a `401` response, and proactively up to five minutes before the stored expiry. To avoid refresh failures:
169193 
170## Enterprise authentication
194* Return RFC 6749-compliant error codes when a refresh token is no longer valid: `invalid_grant`, not `invalid_request` or a custom code
195* Rotate refresh tokens for public-client connections. DCR and CIMD register Claude as a public client, and the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-theft) adopts OAuth 2.1's requirement to rotate or sender-constrain refresh tokens for public clients. If you rotate, return the new refresh token in the same response that invalidates the old one
171196 
172<Note>
173 Organizations using SSO can also connect their users to your server without an interactive OAuth consent step, using an identity assertion signed by their identity provider. See [Enterprise Managed Auth](./enterprise-managed-auth) for what your authorization server needs to support.
174</Note>
197Your `/token` endpoint must accept `Content-Type: application/x-www-form-urlencoded` per [RFC 6749 section 4.1.3](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3). Claude sends both the initial token exchange and refresh requests with this content type. Some web frameworks default to JSON-only body parsing, so if your endpoint returns `415 Unsupported Media Type`, register a form-urlencoded body parser. Dynamic client registration at `/register` uses `application/json` per [RFC 7591 section 3.1](https://www.rfc-editor.org/rfc/rfc7591#section-3.1), so don't assume the same parser works for both.
175198 
176Most directory connectors use a **single shared OAuth application per connector**. Enterprise customers connect to the same OAuth app as everyone else, and access is scoped by the user's own permissions on your service. For a listing that asks for [credentials entered at connection time](#credentials-entered-at-connection-time), each customer supplies its own OAuth client instead. Custom connectors are different: an admin can supply their own OAuth client credentials when adding the connector, which scopes the OAuth client to that organization. See [custom connectors](#custom-connectors).
199### Endpoint latency
177200 
178## Custom connectors
201Claude waits up to 10 seconds for a response from your OAuth discovery, registration, and token endpoints, and up to 30 seconds for refresh token requests. If no response arrives within that window, Claude treats the flow as a failure, even if your server eventually completes the request. Aim well under these limits. A token endpoint that takes several seconds to respond produces intermittent connection failures for users.
179202 
180When a user adds a custom connector by URL, the OAuth Client Secret field is **optional**. Supply it only if your authorization server requires confidential-client authentication.
203If your token endpoint depends on slow downstream calls, return the HTTP response headers and body without buffering behind upstream work, and check that any reverse proxy, API gateway, or WAF in front of the endpoint isn't holding the response.
181204 
182Supplying your own pre-registered client ID (and secret, if your server requires one) as static client credentials is a good option when you want a stable OAuth client per organization: it avoids dynamic client registration entirely, and the credentials are scoped to the organization that entered them.
205## Enterprise and custom connector authentication
183206 
184For servers that authenticate with a fixed API key or token rather than OAuth, request header authentication (`static_headers`) is available in beta. See [Supported authentication types](#supported-authentication-types) above and [Authenticating with request headers](/docs/connectors/custom/remote-mcp#authenticating-with-request-headers) for what administrators see.
207You don't need a separate OAuth application for each enterprise customer, but a customer's organization can supply its own OAuth client or connect its users through SSO without a consent screen.
185208 
186## Endpoint latency
209### Enterprise authentication
187210 
188Claude waits up to **10 seconds** for a response from your OAuth discovery, registration, and token endpoints, and up to **30 seconds** for refresh token requests. If no response arrives within that window the flow is treated as a failure, even if your server eventually completes the request. Aim well under these limits; a token endpoint that takes several seconds to respond will produce intermittent connection failures for users.
211Unless your listing asks for credentials at connection time, enterprise customers connect through the same OAuth application as everyone else. Scope what each user can reach with your service's own per-user permissions rather than with a per-tenant OAuth app. These cases work differently:
189212 
190If your token endpoint depends on slow downstream calls, return the HTTP response headers and body without buffering behind upstream work, and check that any reverse proxy, API gateway, or WAF in front of the endpoint isn't holding the response.
213* **SSO without a consent screen**: organizations using SSO can connect their users with an identity assertion signed by their identity provider instead of an interactive OAuth consent step. See [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth) for what your authorization server needs to support
214* **Customer-supplied OAuth client**: a listing that asks for [credentials entered at connection time](#credentials-entered-at-connection-time) has each customer supply its own OAuth client, and an administrator who adds your server as a [custom connector](#custom-connectors) can enter one too
191215 
216### Custom connectors
217 
218When a customer adds your server by URL as a custom connector, Claude identifies itself to your authorization server in one of three ways: with the Client ID Metadata Document that Anthropic hosts for it, through Dynamic Client Registration, or with an OAuth client ID the customer registered with you and enters in the dialog. [Choose authentication settings](/docs/connectors/custom/add-unlisted#choose-authentication-settings) shows the dialog the customer sees. On your side:
219 
220* **Support CIMD or DCR**: customers can then connect without registering a client with you first. See [DCR and CIMD details](#dcr-and-cimd-details) for what each needs from your authorization server
221* **Require a client secret only for confidential clients**: the secret is optional in the dialog, so customers need one only if your authorization server requires confidential-client authentication
222* **Use request headers for a fixed API key or token**: for servers that authenticate with a fixed credential rather than OAuth, request header authentication (`static_headers`) is available in beta. See [Supported authentication types](#supported-authentication-types) and [Authenticate with request headers](/docs/connectors/custom/add-unlisted#authenticate-with-request-headers) for what Owners see
223 
192224## Network reference
193225 
194226Anthropic's outbound traffic to your server originates from `160.79.104.0/21`. See the [IP address reference](https://platform.claude.com/docs/en/api/ip-addresses) if you need to allowlist Anthropic for conditional access or firewall rules.
227 
228## Next steps
229 
230* [Lazy authentication](/docs/connectors/building/lazy-authentication): let people use the tools that don't need their account right away, and ask them to sign in only when Claude reaches one that does
231* [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth): accept identity assertions from enterprise SSO instead of an interactive consent step
232* [Test your connector](/docs/connectors/building/testing): add your server as a custom connector and exercise the auth flow
233* [Troubleshoot your connector](/docs/connectors/building/troubleshooting): diagnose "Couldn't reach the MCP server" and "Authorization with the MCP server failed"
195234 

connectors/building/directory-vs-custom Changed · +50 / -26 lines

## Compare directory and custom connectors ### Directory connector listing URL ### Custom connector install link ## Understand how listing affects discovery ### Suggested Connectors ### The MCP Registry and the Anthropic Directory ## Listing patterns for enterprise and multi-tenant servers ### Offer a listing and a custom connector ### Per-tenant URLs ## Next steps ### Directory connectors ### Custom connectors ## Suggested Connectors ## Use both: directory plus elevated custom ## Per-tenant URLs ## What the directory is not

from line 1
11# Directory connectors vs custom connectors
22 
3> Understand the difference between directory-listed and custom connectors
3> Compare directory-listed and custom connectors: Anthropic review, in-product discovery, install links, Suggested Connectors, and per-tenant URL listings.
44 
5Directory connectors and custom connectors run on the **same MCP infrastructure**. The runtime, transport, authentication, and tool-calling code paths are identical. The difference is review, discoverability, and distribution.
5A directory connector is an MCP server listed in Anthropic's directory after review, and a custom connector is one that a user or an organization Owner adds to Claude by entering its URL. Both run on the same MCP infrastructure: the runtime, transport, authentication, and tool-calling code paths are identical. The difference is review, discoverability, and distribution.
66 
7This page is for developers deciding whether to list a server in the directory, distribute it as a custom connector, or do both. It compares the two, shows the install link each one gets, and covers listing patterns for enterprise and multi-tenant servers.
8 
9<Note>
10 If you want to know what directory and custom connectors look like to a Claude user, including the Verified and Community labels, see [connector verification](/docs/connectors/verification).
11</Note>
12 
13## Compare directory and custom connectors
14 
15Directory and custom connectors differ only in what surrounds the runtime: who reviews the server, how users find it, and which Anthropic-side features it can use.
16 
717| | Directory connector | Custom connector |
818| ------------------------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------------------------- |
919| **Runtime** | Same | Same |
from line 22
1222| **Distribution** | [Directory link](#share-an-install-link) | [Install link](#share-an-install-link) or manual URL entry |
1323| **Anthropic-held client credentials** | Available | Not available |
1424| **[External link](/docs/connectors/building/mcp-apps/external-links) confirmation** | Can allowlist destinations to skip the modal | Always shows the modal |
15| **Appears as** | Named card with logo | "Custom" |
25| **Appears as** | Named card with logo | **Custom** |
1626 
17For what directory and custom connectors look like to a Claude user, including the Verified and Community labels, see [connector verification](/docs/connectors/verification).
18 
1927## Share an install link
2028 
21Both directory and custom connectors have a URL you can share from your own documentation, a "Connect to Claude" button, or an onboarding email.
29Both directory and custom connectors have a URL you can share from your own documentation, a **Connect to Claude** button, or an onboarding email.
2230 
23### Directory connectors
31### Directory connector listing URL
2432 
2533After publication, your connector has a permanent listing URL based on its slug:
2634 
from line 36
2836https://claude.ai/directory/connectors/SLUG
2937```
3038 
31For example, `https://claude.ai/directory/connectors/dovetail` opens the Dovetail listing with its description, screenshots, and a **Connect** button. You receive your slug when your submission is approved, and it [cannot change afterward](/docs/connectors/building/after-publishing#slugs-are-permanent).
39For example, `https://claude.ai/directory/connectors/dovetail` opens the Dovetail listing with its description, screenshots, and a **Connect** button. You receive your slug when your submission is approved, and it [can't change afterward](/docs/connectors/building/after-publishing#directory-listing-urls-are-permanent).
3240 
33### Custom connectors
41### Custom connector install link
3442 
35For a connector that is not in the directory, link to the **Add custom connector** dialog with the name and URL prefilled:
43For a connector that isn't in the directory, link to the **Add custom connector** dialog with the name and URL prefilled:
3644 
3745```text theme={null}
3846https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=NAME&connectorUrl=ENCODED_URL
3947```
4048 
41| Parameter | Description |
42| --------------- | ----------------------------------------------------------------------------------------------------------- |
43| `modal` | Must be `add-custom-connector`. |
44| `connectorName` | Display name shown to the user. |
45| `connectorUrl` | Your MCP server URL, [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding). |
49The link takes these query parameters:
4650 
51| Parameter | Description |
52| --------------- | ---------------------------------------------------------------------------------------------------------- |
53| `modal` | Must be `add-custom-connector` |
54| `connectorName` | Display name shown to the user |
55| `connectorUrl` | Your MCP server URL, [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding) |
56 
4757For example, an install link for a server at `https://mcp.example.com/` looks like this:
4858 
4959```text theme={null}
from line 60
5060https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Example&connectorUrl=https%3A%2F%2Fmcp.example.com%2F
5161```
5262 
53When a user follows the link, claude.ai opens the dialog with the name and URL prefilled and shows a notice that the values came from an external link. The user reviews the values and confirms before anything is added. If the user is signed out, they are prompted to sign in first and then land on the prefilled dialog.
63When a user follows the link, claude.ai opens the dialog with the name and URL prefilled and shows a notice that the values came from an external link. The user reviews the values and confirms before anything is added. A signed-out user is prompted to sign in first and then sees the prefilled dialog.
5464 
5565<Note>
56 Install links only prefill the form. They do not bypass review by the user, and they do not grant your server any permissions the user has not confirmed.
66 Install links only prefill the form. They don't bypass review by the user, and they don't grant your server any permissions the user hasn't confirmed.
5767</Note>
5868 
59Organization administrators can use the same parameters on the admin path to prefill the org-wide connector dialog:
69Organization Owners can use the same parameters on the admin path to prefill the org-wide connector dialog:
6070 
6171```text theme={null}
6272https://claude.ai/admin-settings/connectors?modal=add-custom-connector&connectorName=NAME&connectorUrl=ENCODED_URL
6373```
6474 
65## Suggested Connectors
75## Understand how listing affects discovery
6676 
67Directory connectors are eligible for **Suggested Connectors**—Claude can recommend your connector in-chat when it's relevant to the user's task. Custom connectors are never suggested. Every directory entry is automatically eligible; there is no separate opt-in.
77Only a directory listing makes your connector discoverable inside Claude. Listings elsewhere, including the open MCP Registry, don't.
6878 
69## Use both: directory plus elevated custom
79### Suggested Connectors
7080 
71A supported pattern is to list a connector in the directory with safe, broadly-applicable defaults, **and** provide enterprise customers a separate URL to add as a custom connector with elevated permissions or tenant-specific configuration. Document both paths in your own product docs.
81Directory connectors are eligible for Suggested Connectors, which means Claude can recommend your connector in-chat when it's relevant to the user's task. Custom connectors are never suggested. Every directory entry is automatically eligible, and there is no separate opt-in.
7282 
73## Per-tenant URLs
83### The MCP Registry and the Anthropic Directory
7484 
75If your server URL varies per tenant (for example, `{tenant}.mcp.example.com`), submit one directory listing with a URL pattern. Each user enters their own URL when they connect. See [Servers with per-customer URLs](/docs/connectors/building/authentication#servers-with-per-customer-urls) for how this choice limits your authentication options.
85The Anthropic Directory is independent of the open [MCP Registry](https://registry.modelcontextprotocol.io) and the `modelcontextprotocol/servers` GitHub repository. Publishing to those doesn't surface your server in Claude. Submit through the [directory submission form](/docs/connectors/building/submission) to appear in Claude products.
7686 
77## What the directory is not
87## Listing patterns for enterprise and multi-tenant servers
7888 
79The Anthropic Directory is independent of the open [MCP Registry](https://registry.modelcontextprotocol.io) and the `modelcontextprotocol/servers` GitHub repository. Publishing to those does **not** surface your server in Claude. Submit through the [directory submission form](/docs/connectors/building/submission) to appear in Claude products.
89A single directory listing can still serve customers who need elevated permissions or their own server URL.
90 
91### Offer a listing and a custom connector
92 
93A supported pattern is to list a connector in the directory with safe, broadly applicable defaults, and provide enterprise customers a separate URL to add as a custom connector with elevated permissions or tenant-specific configuration. Document both paths in your own product docs.
94 
95### Per-tenant URLs
96 
97If your server URL varies per tenant, such as `{tenant}.mcp.example.com`, submit one directory listing with a URL pattern. Each user enters their own URL when they connect. See [Servers with per-customer URLs](/docs/connectors/building/authentication#servers-with-per-customer-urls) for how this choice limits your authentication options.
98 
99## Next steps
100 
101* [Authentication for connectors](/docs/connectors/building/authentication): the authentication types available to directory and custom connectors
102* [Publish to the directory](/docs/directory/publish): who can submit, what review involves, and where to start
103* [Add a connector by URL](/docs/connectors/custom/add-unlisted#add-a-connector-by-url): how users and Owners add a custom connector by URL
80104 

connectors/building/enterprise-managed-auth Changed · +47 / -72 lines

## Understand how Enterprise Managed Auth works ### Enterprise Managed Auth with lazy authentication ### Access token lifetime ## Test your implementation ### Test with the cross-app access playground ## Support customer administrators ### Admin settings in your product ### Provide setup documentation ### Okta Integration Network apps ## How it works ## With lazy authentication ## Access token lifetime ## Testing your implementation ### Testing with the cross-app access playground ## Admin settings in your product ## Provide setup documentation ## Okta Integration Network apps

from line 6
66 Enterprise Managed Auth is available on Claude Team and Enterprise plans. MCP server developers and identity provider vendors can [register interest](https://docs.google.com/forms/d/e/1FAIpQLSf1goHGNDVFK7rncYuh6wnRpWSy7eGOcgL1i8uw3oyKFO9UUA/viewform) in supporting this flow.
77</Note>
88 
9Enterprise Managed Auth (EMA) lets a user connect to your MCP server silently, using the single sign-on session they already have with their organization. Instead of showing each user an OAuth consent screen, Claude presents your authorization server with an **identity assertion**: a signed JSON Web Token (JWT), issued by the customer's identity provider, that vouches for the user's identity.
9Enterprise Managed Auth (EMA) lets a user connect to your MCP server silently, using the single sign-on session they already have with their organization. Instead of showing each user an OAuth consent screen, Claude presents your authorization server with an identity assertion: a signed JSON Web Token (JWT), issued by the customer's identity provider, that vouches for the user's identity.
1010 
11Your authorization server validates the assertion and returns an access token in a single back-channel request. There is no browser redirect and no per-connector consent page. From the user's point of view, the connector is simply available as soon as their administrator enables it.
11Your authorization server validates the assertion and returns an access token in a single back-channel request. There is no browser redirect and no per-connector consent page. From the user's point of view, the connector is available as soon as their administrator enables it.
1212 
13This flow is defined by the [MCP enterprise managed authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) and is built on the standard [JWT bearer authorization grant (RFC 7523)](https://datatracker.ietf.org/doc/html/rfc7523). The assertion profile follows the [Identity Assertion JWT Authorization Grant](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/).
13This page is for connector developers who need their authorization server to accept Enterprise Managed Auth. The customer's administrator handles identity provider setup and Claude admin console configuration, so those aren't covered here. The page walks through [how the exchange works](#understand-how-enterprise-managed-auth-works), the [prerequisites](#prerequisites), [what your authorization server must support](#authorization-server-requirements), [how to test](#test-your-implementation), and [what to give customer administrators](#support-customer-administrators).
1414 
15<Note>
16 This page is for connector developers who need their authorization server to accept Enterprise Managed Auth. Identity provider setup and Claude admin console configuration are handled by the customer's administrator.
17</Note>
15## Understand how Enterprise Managed Auth works
1816 
19## How it works
17When a user whose organization has Enterprise Managed Auth configured invokes your connector, Claude obtains a signed identity assertion for that user and exchanges it directly at your token endpoint for an access token. The user never sees a browser redirect or a consent screen, and your MCP server receives the same kind of bearer token it would after the interactive OAuth flow. The diagram shows the exchange between Claude, your authorization server, and your MCP server.
2018 
21When a user whose organization has Enterprise Managed Auth configured invokes your connector, Claude obtains a signed identity assertion for that user and exchanges it directly at your token endpoint for an access token. The user never sees a browser redirect or a consent screen, and your MCP server receives the same kind of bearer token it would after the interactive OAuth flow.
19<img className="block dark:hidden" src="https://mintcdn.com/claude-ai/-njlLvrWxFCRdJVz/images/connectors/enterprise-managed-auth-exchange.svg?fit=max&auto=format&n=-njlLvrWxFCRdJVz&q=85&s=2bdd0b680bda337b37632516b8dc5e84" alt="Sequence diagram with three participants: Claude, your authorization server, and your MCP server. The user is already signed in to Claude through their organization's single sign-on. 1, Claude sends POST /token to your authorization server with the jwt-bearer grant and the signed JWT assertion. 2, your authorization server fetches the issuer's JWKS and verifies the signature. 3, it validates iss, aud, exp, sub, and client_id. 4, it returns an access_token to Claude. 5, Claude sends the tool call to your MCP server with the Bearer access_token. 6, your MCP server returns the tool result to Claude. Requests are solid arrows and responses are dashed arrows." width="1000" height="560" data-path="images/connectors/enterprise-managed-auth-exchange.svg" />
2220 
23```mermaid theme={null}
24sequenceDiagram
25 autonumber
26 participant Claude
27 participant AS as Your authorization server
28 participant MCP as Your MCP server
21<img className="hidden dark:block" src="https://mintcdn.com/claude-ai/-njlLvrWxFCRdJVz/images/connectors/enterprise-managed-auth-exchange-dark.svg?fit=max&auto=format&n=-njlLvrWxFCRdJVz&q=85&s=5b8c6c81ef448cfb08deb05f0d02b712" alt="Sequence diagram with three participants: Claude, your authorization server, and your MCP server. The user is already signed in to Claude through their organization's single sign-on. 1, Claude sends POST /token to your authorization server with the jwt-bearer grant and the signed JWT assertion. 2, your authorization server fetches the issuer's JWKS and verifies the signature. 3, it validates iss, aud, exp, sub, and client_id. 4, it returns an access_token to Claude. 5, Claude sends the tool call to your MCP server with the Bearer access_token. 6, your MCP server returns the tool result to Claude. Requests are solid arrows and responses are dashed arrows." width="1000" height="560" data-path="images/connectors/enterprise-managed-auth-exchange-dark.svg" />
2922 
30 Note over Claude: User is signed in to Claude through their organization's SSO
31 Claude->>AS: POST /token (grant_type=jwt-bearer, assertion=signed JWT)
32 AS->>AS: Fetch issuer JWKS and verify signature
33 AS->>AS: Validate iss, aud, exp, sub, and client_id
34 AS-->>Claude: access_token
35 Claude->>MCP: Tool call with Bearer access_token
36 MCP-->>Claude: Tool result
37```
38 
3923Two parties are involved in this exchange: the customer's identity provider signs the assertion, and your authorization server verifies it and issues the access token. Both roles are often served by commercial identity platforms, so the distinction here is about which tenant plays which role rather than about product type. The identity provider publishes its signing keys as a JSON Web Key Set, and your authorization server fetches that key set to verify each assertion.
4024 
41## With lazy authentication
25The Enterprise Managed Auth flow is defined by the [MCP enterprise managed authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) and is built on the standard [JWT bearer authorization grant (RFC 7523)](https://datatracker.ietf.org/doc/html/rfc7523). The assertion profile follows the [Identity Assertion JWT Authorization Grant](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/).
4226 
43Enterprise Managed Auth also works with [lazy authentication](./lazy-authentication). When your server returns `401 Unauthorized` for a protected tool call, Claude normally shows the inline **Connect** card and runs the interactive OAuth flow. If the user's organization has Enterprise Managed Auth configured for your connector, Claude runs the silent JWT bearer exchange instead and retries the tool call without showing a prompt.
27### Enterprise Managed Auth with lazy authentication
4428 
45Your MCP server returns the same `401` with a `WWW-Authenticate` header as described in the [lazy authentication guide](./lazy-authentication#return-401-not-a-tool-error). Your authorization server must still meet the [requirements below](#authorization-server-requirements).
29Enterprise Managed Auth also works with [lazy authentication](/docs/connectors/building/lazy-authentication). When your server returns `401 Unauthorized` for a protected tool call, Claude normally shows the inline **Connect** card and runs the interactive OAuth flow. If the user's organization has Enterprise Managed Auth configured for your connector, Claude runs the silent JWT bearer exchange instead and retries the tool call without showing a prompt.
4630 
47A fully authless server never returns `401`, so there is no point at which Claude can exchange an assertion. Enterprise Managed Auth does not apply to authless servers.
31Your MCP server returns the same `401` with a `WWW-Authenticate` header as described in the [lazy authentication guide](/docs/connectors/building/lazy-authentication#answer-a-protected-call-with-401-before-the-mcp-sdk-runs). Your authorization server must still meet the [authorization server requirements](#authorization-server-requirements).
4832 
33Enterprise Managed Auth doesn't apply to authless servers. A fully authless server never returns `401`, so there is no point at which Claude can exchange an assertion.
34 
4935## Prerequisites
5036 
51Before adding Enterprise Managed Auth, make sure the following are already in place.
37Before adding Enterprise Managed Auth, make sure the following are already in place:
5238 
53* Your MCP server implements [MCP authorization](https://modelcontextprotocol.io/specification/draft/basic/authorization), including Protected Resource Metadata (PRM) discovery, and follows the [MCP security best practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices). See our [authentication guide](./authentication) for Claude-specific requirements.
54* Your authorization server registers Claude using either [Anthropic-held client credentials](./authentication#anthropic-held-client-credentials) or a [Client ID Metadata Document](./authentication#dcr-and-cimd-details).
39* Your MCP server implements [MCP authorization](https://modelcontextprotocol.io/specification/draft/basic/authorization), including Protected Resource Metadata (PRM) discovery, and follows the [MCP security best practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices). See the [authentication guide](/docs/connectors/building/authentication) for Claude-specific requirements
40* Your authorization server registers Claude using either [Anthropic-held client credentials](/docs/connectors/building/authentication#anthropic-held-client-credentials) or a [Client ID Metadata Document](/docs/connectors/building/authentication#dcr-and-cimd-details)
5541 
5642<Warning>
57 Dynamic Client Registration (DCR) is not supported with Enterprise Managed Auth. The identity provider stamps a fixed `client_id` into every assertion it issues, so your authorization server must already recognize that client before the first assertion arrives. A client created on the fly through DCR cannot satisfy this requirement because its identifier will never match the value in the assertion.
43 Dynamic Client Registration (DCR) isn't supported with Enterprise Managed Auth. The identity provider stamps a fixed `client_id` into every assertion it issues, so your authorization server must already recognize that client before the first assertion arrives. A client created on the fly through DCR can't satisfy this requirement because its identifier never matches the value in the assertion.
5844</Warning>
5945 
6046## Authorization server requirements
6147 
62<Note>
63 This section is for the authorization server operator. If your MCP server relies on a hosted identity platform, there is typically no code to write. Confirm that the platform supports the JWT bearer authorization grant and enable it for your tenant. If you run your own authorization server, the steps below describe what it needs to support.
64</Note>
48Your authorization server must support the JWT bearer authorization grant ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)), which lets an authorization server exchange a signed JWT for an access token, and must trust each customer's identity provider as an issuer. This section is for the authorization server operator.
6549 
66Support for this flow varies by product. The underlying capability is the JWT bearer authorization grant ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)), which lets an authorization server exchange a signed JWT for an access token. Some commercial authorization servers and identity platforms support it today and others do not yet, so the first step is to confirm that yours does and that the customer's identity provider can be registered as a trusted issuer.
50If your MCP server relies on a hosted identity platform, there is typically no code to write. Confirm that the platform supports the JWT bearer authorization grant and enable it for your tenant. Support varies by product, and some commercial authorization servers and identity platforms don't support it yet, so confirm that yours does and that the customer's identity provider can be registered as a trusted issuer. If you run your own authorization server, the steps in this section describe what it needs to support.
6751 
6852<Steps>
6953 <Step title="Ensure the JWT bearer grant is supported">
70 Your authorization server must accept `urn:ietf:params:oauth:grant-type:jwt-bearer` at its token endpoint and advertise it in the `grant_types_supported` array of its [authorization server metadata (RFC 8414)](https://datatracker.ietf.org/doc/html/rfc8414):
54 Your authorization server must accept `urn:ietf:params:oauth:grant-type:jwt-bearer` at its token endpoint and advertise it in the `grant_types_supported` array of its [authorization server metadata (RFC 8414)](https://datatracker.ietf.org/doc/html/rfc8414). In this example metadata, the highlighted entry is the one Claude looks for:
7155 
72 ```json theme={null}
56 ```json {7} theme={null}
7357 {
7458 "issuer": "https://auth.example.com",
7559 "token_endpoint": "https://auth.example.com/token",
from line 65
8165 }
8266 ```
8367 
84 Claude reads this metadata to discover whether your server supports Enterprise Managed Auth. The grant type must be listed here for the feature to be offered to the customer, even if your token endpoint would already accept it silently.
68 Claude reads this metadata to discover whether your server supports Enterprise Managed Auth. The grant type must be listed here for Claude to offer the feature to the customer, even if your token endpoint would already accept it silently.
8569 </Step>
8670 
8771 <Step title="Register the trusted issuer">
8872 For each customer, your authorization server needs to trust that customer's identity provider as a JWT issuer. Your authorization server fetches the identity provider's JSON Web Key Set and uses it to verify the signature on every incoming assertion.
8973 
90 Your authorization server is responsible for maintaining an explicit allowlist of trusted issuer URLs per tenant rather than accepting any well-formed JWT. An assertion whose `iss` is not on the tenant's allowlist must be rejected with `invalid_grant`, even if the signature is valid.
74 Your authorization server is responsible for maintaining an explicit allowlist of trusted issuer URLs per tenant rather than accepting any well-formed JWT. It must reject an assertion whose `iss` isn't on the tenant's allowlist with `invalid_grant`, even if the signature is valid.
9175 
9276 <Warning>
93 Never accept an identity assertion without full validation. As with all OAuth token handling, your authorization server must verify the signature, issuer, audience, expiry, and subject on every request. Use the JWT validation built into your authorization server product. If you need to inspect assertions in your own code, use the validation library or token introspection endpoint provided by your authorization server vendor rather than writing custom verification logic.
77 Never accept an identity assertion without full validation. Your authorization server must verify the signature, issuer, audience, expiry, and subject on every request. Use the JWT validation built into your authorization server product. If you need to inspect assertions in your own code, use the validation library or token introspection endpoint provided by your authorization server vendor rather than writing custom verification logic.
9478 </Warning>
9579 </Step>
9680 
9781 <Step title="Understand the token request">
98 Claude sends a form-encoded `POST` to your authorization server's token endpoint:
82 Claude sends a form-encoded `POST` to your authorization server's token endpoint. The highlighted parameters are the JWT bearer grant type, the signed assertion, and the `client_id` your server must already recognize:
9983 
100 ```http theme={null}
84 ```http {5-7} theme={null}
10185 POST /token HTTP/1.1
10286 Host: auth.example.com
10387 Content-Type: application/x-www-form-urlencoded
from line 98
11498 Your authorization server validates the assertion according to the [JWT bearer token processing rules (RFC 7523 section 3)](https://datatracker.ietf.org/doc/html/rfc7523#section-3) and returns a standard OAuth token response. Claude then presents the returned access token as a `Bearer` credential on calls to your MCP server, exactly as it does after the interactive flow.
11599 
116100 <Note>
117 The access token lifetime is set by your authorization server, and the assertion lifetime is set by the customer's identity provider. Anthropic does not control either value.
101 Your authorization server sets the access token lifetime, and the customer's identity provider sets the assertion lifetime. Anthropic doesn't control either value.
118102 </Note>
119103 </Step>
120104</Steps>
121105 
122## Access token lifetime
106### Access token lifetime
123107 
124Issue access tokens with whatever lifetime your security policy calls for. A short lifetime, such as one hour, does not force users to repeat single sign-on each time a token expires.
108Issue access tokens with whatever lifetime your security policy calls for. A short lifetime, such as one hour, doesn't force users to repeat single sign-on each time a token expires.
125109 
126110When a user signs in to Claude through their organization's SSO, Claude obtains a long-lived refresh token from the identity provider. Claude uses that refresh token to request a fresh identity assertion from the identity provider whenever it needs one, without any user interaction. Claude then exchanges the new assertion at your token endpoint for a new access token. From the user's point of view, the connection stays active for as long as the identity provider's refresh token remains valid.
127111 
128The refresh token is issued by the customer's identity provider. Treat it as long-lived today. Identity provider administrators will be able to apply their own lifetime policy in the future.
112The customer's identity provider issues the refresh token. Treat it as long-lived.
129113 
130## Testing your implementation
114## Test your implementation
131115 
132116End-to-end testing requires a Claude organization with Enterprise Managed Auth enabled and an identity provider tenant configured to issue assertions for your authorization server's audience.
133117 
134If your identity provider is Okta, refer to Okta's [Cross App Access participation guide](https://support.okta.com/help/s/article/claude-enterprise-managed-auth-with-okta-cross-app-access-xaa-beta-participation-guide?language=en_US) and configure your organization to be able to test your MCP's XAA implementation.
118If your identity provider is Okta, refer to Okta's [Cross App Access participation guide](https://support.okta.com/help/s/article/claude-enterprise-managed-auth-with-okta-cross-app-access-xaa-beta-participation-guide?language=en_US) and configure your organization so you can test your MCP server's Cross App Access (XAA) implementation.
135119 
136### Testing with the cross-app access playground
120### Test with the cross-app access playground
137121 
138[Okta's cross-app access playground](https://xaa.dev) lets you exercise the flow without a Claude organization. This is useful while you develop, and when your organization's single sign-on is not on a supported identity provider. On the playground you can:
122[Okta's cross-app access playground](https://xaa.dev) lets you exercise the flow without a Claude organization. The playground is useful while you develop, and when your organization's single sign-on isn't on a supported identity provider. On the playground you can do the following:
139123 
140* Walk the full four-step flow end to end against a sandbox IdP, with every token shown decoded
124* Walk the full flow end to end against a sandbox IdP, with every token shown decoded
141125* Point it at your own MCP server or REST API to check resource metadata discovery, the `WWW-Authenticate` hint on `401` responses, and token validation
142126* Point it at your own authorization server to check that it accepts an identity assertion over the JWT bearer grant, mints a scoped access token, and serves its metadata for discovery
143127* Bring your own OIDC or SAML identity provider in place of the sandbox one
144128* Re-run a single failed step and inspect service configurations, discovery documents, and a live event log
145129 
146## Admin settings in your product
130## Support customer administrators
147131 
132A customer's administrator turns on Enterprise Managed Auth for their organization, so your product needs an admin control for it, setup documentation they can follow, and, for Okta customers, an app that supports Cross App Access.
133 
134### Admin settings in your product
135 
148136In your product's admin settings, give each customer's administrator a control that turns Enterprise Managed Auth on or off for their organization and a field for their identity provider's issuer URL. When the administrator saves, add that URL to the organization's allowlist of trusted issuers, as described in [Authorization server requirements](#authorization-server-requirements).
149137 
150## Provide setup documentation
138### Provide setup documentation
151139 
152You can publish documentation that walks an enterprise administrator through enabling Enterprise Managed Auth for your product and add its URL to [your directory listing](./managing-your-listing). Claude shows the link in the Claude admin console when an administrator sets up Enterprise Managed Auth for your connector.
140You can publish documentation that walks an enterprise administrator through enabling Enterprise Managed Auth for your product and add its URL to [your directory listing](/docs/connectors/building/managing-your-listing). Claude shows the link in the Claude admin console when an administrator sets up Enterprise Managed Auth for your connector.
153141 
154## Okta Integration Network apps
142### Okta Integration Network apps
155143 
156144If your product has an app in the Okta Integration Network, work with Okta to enable Cross App Access (XAA) for that app. Until that app supports Cross App Access, customers who use Okta need to create a custom app in Okta for your product before they can set up Enterprise Managed Auth.
157145 
158146## Related resources
159147 
160<CardGroup cols={2}>
161 <Card title="Authentication for connectors" icon="key" href="./authentication">
162 Baseline OAuth requirements your server must already meet.
163 </Card>
164 
165 <Card title="Lazy authentication" icon="lock-open" href="./lazy-authentication">
166 Defer OAuth until a protected tool is actually invoked.
167 </Card>
168 
169 <Card title="Testing your connector" icon="flask" href="./testing">
170 Verify your connector works end to end in Claude.
171 </Card>
172 
173 <Card title="Troubleshooting" icon="wrench" href="./troubleshooting">
174 Diagnose common authentication and connection issues.
175 </Card>
176</CardGroup>
148* [Authentication for connectors](/docs/connectors/building/authentication): baseline OAuth requirements your server must already meet
149* [Lazy authentication](/docs/connectors/building/lazy-authentication): ask users to sign in only when Claude reaches a tool that needs their account
150* [Test your connector](/docs/connectors/building/testing): verify your connector works end to end in Claude
151* [Troubleshoot your connector](/docs/connectors/building/troubleshooting): diagnose common authentication and connection issues
177152 

connectors/building/index Changed · +72 / -48 lines

# Build an MCP server for Claude ## Plan your server ### Choose where the server runs ### Choose how users authenticate ### Decide what the server exposes ### Design within the size and timeout limits ### Decide whether to add interactive UI ## Test your server against Claude ## Decide how people get your server ## Related resources ## Next steps # Building custom connectors ## Getting started ### Key resources ## Transport & authentication ### Supported transports ### Authentication features ## Protocol features ### Supported ### Not yet supported ## Technical specifications ## Testing your server ## Related topics

from line 1
1# Building custom connectors
1# Build an MCP server for Claude
22 
3> Build your own MCP servers to connect Claude to your tools and data
3> Build a remote MCP server that people use as a connector in Claude: where it runs, authentication, what it exposes, size and timeout limits, and distribution.
44 
5## Getting started
5An MCP server gives Claude access to your product or data, and people who use Claude see it as a connector. It's the piece of your [plugin](/docs/build/overview) that reaches your product: the plugin's `.mcp.json` points at the server you build and host, and any skills you include teach Claude how to use it. Claude connects to your server from claude.ai, Claude Desktop, Claude mobile, Cowork, and Claude Code, and the same connector infrastructure backs all of them.
66 
7This page is for developers building a remote MCP server for other people to use in Claude. It covers the decisions you make as you build, in the order you meet them, and what Claude's MCP client supports for each one.
8 
79<Note>
8 **Authentication is the most common stumbling block.** Before you build, read the [authentication reference](/docs/connectors/building/authentication)—Claude's auth support differs from the generic MCP spec in a few important ways.
10 * If you're new to the protocol itself, see [Model Context Protocol (MCP)](/docs/connectors/building/mcp) for a short orientation
11 * If you want to build a minimal server first and watch Claude call it, see [Build your first MCP server for Claude](/docs/connectors/building/quickstart)
12 * If you're not sure your plugin needs an MCP server, see [Decide what to include in your plugin](/docs/connectors/building/what-to-build)
913</Note>
1014 
11Not sure whether to build an MCP server, a plugin, or both? See [what to build](/docs/connectors/building/what-to-build).
15## Plan your server
1216 
17Claude implements a subset of the MCP specification, with its own callback URL and its own size and timeout limits.
18 
1319<Tip>
14 **Build with Claude.** Install the official [`mcp-server-dev` plugin](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev) in Claude Code—it walks you through building, testing, and packaging an MCP server interactively, using these docs as its reference.
20 To build with Claude's help, install the official [`mcp-server-dev` plugin](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev) in Claude Code. It walks you through building, testing, and packaging an MCP server interactively, using these docs as its reference.
1521</Tip>
1622 
17### Key resources
23### Choose where the server runs
1824 
19* **SDK Examples**: [TypeScript](https://github.com/modelcontextprotocol/typescript-sdk) and [Python](https://github.com/modelcontextprotocol/python-sdk) SDKs contain server implementation examples
20* **Protocol Specification**: [modelcontextprotocol.io](https://modelcontextprotocol.io)
21* **Hosting Solutions**: Platforms like Cloudflare offer remote MCP server hosting with autoscaling and OAuth management
22* **Auth Specifications**: Review the [authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization) with emphasis on third-party service flows
25A remote server runs on infrastructure you host and is reachable over the internet. Claude connects to it from every Claude app, and a remote server is the recommended kind for a directory listing.
2326 
24## Transport & authentication
27A local server runs on the user's computer. To package one for the Claude desktop app, see [Build a desktop extension with MCPB](/docs/connectors/building/mcpb).
2528 
26### Supported transports
29A transport is how Claude and your server exchange MCP messages. Use [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports/streamable-http), which the MCP specification defines for remote servers. Claude also supports the legacy [HTTP+SSE transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse), which is being deprecated in favor of Streamable HTTP.
2730 
28Claude supports both Streamable HTTP and the legacy HTTP+SSE transport. The legacy HTTP+SSE transport is being deprecated in favor of Streamable HTTP.
31### Choose how users authenticate
2932 
30### Authentication features
33Decide on authentication before you write tool code. Claude's OAuth client differs from the generic MCP specification in a few places.
3134 
32* Supports the [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), and [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) auth specifications
33* Dynamic Client Registration (DCR) enabled
34* OAuth callback: `https://claude.ai/api/mcp/auth_callback` (hosted surfaces); loopback redirect for Claude Code — see [callback URLs](/docs/connectors/building/authentication#callback-urls)
35* Token refresh and expiry support
36* Custom credentials for non-DCR servers
35Your server can let Claude in with OAuth 2.0, where each user signs in with their own account; with a static credential that an organization Owner enters once and Claude sends as a request header; or with no authentication at all. [Supported authentication types](/docs/connectors/building/authentication#supported-authentication-types) lists each type and which ones you contact Anthropic to use.
3736 
38## Protocol features
37If you use OAuth, check these parts of your setup against Claude's client:
3938 
40### Supported
39* **Specification version**: Claude follows the [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), and [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) authorization specifications
40* **Client registration**: Claude can register itself with your authorization server through Dynamic Client Registration (DCR). If your server doesn't support DCR, [Register Claude as an OAuth client](/docs/connectors/building/authentication#register-claude-as-an-oauth-client) lists the other ways to give Claude a client identity
41* **Redirect URI**: allow `https://claude.ai/api/mcp/auth_callback` for the hosted surfaces and a loopback redirect for Claude Code, as [Callback URLs](/docs/connectors/building/authentication#callback-urls) describes
42* **Token refresh**: Claude refreshes access tokens when they expire. [Token refresh](/docs/connectors/building/authentication#token-refresh) has the requirements for your token endpoint
4143 
44If you need one of these related flows, follow its page:
45 
46* **[Lazy authentication](/docs/connectors/building/lazy-authentication)**: if some of your tools work without the user's account, people can use those right away and sign in only when Claude reaches a tool that needs it
47* **[Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth)**: lets enterprise users connect through their organization's SSO without a consent screen
48 
49### Decide what the server exposes
50 
51Your server can expose these to Claude:
52 
4253* [Tools](https://modelcontextprotocol.io/specification/latest/server/tools), [prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), and [resources](https://modelcontextprotocol.io/specification/latest/server/resources)
4354* [Text](https://modelcontextprotocol.io/specification/latest/schema#textcontent) and [image-based](https://modelcontextprotocol.io/specification/latest/server/tools#image-content) tool results
4455* [Text](https://modelcontextprotocol.io/specification/latest/schema#textresourcecontents) and [binary](https://modelcontextprotocol.io/specification/latest/schema#blobresourcecontents) resources
4556 
46### Not yet supported
57Claude doesn't yet support these MCP features, so don't build a feature that depends on them:
4758 
4859* Resource subscriptions
4960* Sampling
50* Advanced/draft capabilities
61* Advanced or draft capabilities
5162 
52## Technical specifications
63If you plan to list the server in the directory, [Design tools that pass review](/docs/connectors/building/review-criteria#design-tools-that-pass-review) covers how to name, describe, and annotate tools.
5364 
54| Constraint | Limit |
55| -------------------------------------- | -------------------------------------------------------- |
56| Claude.ai/Desktop max tool result size | \~150,000 characters |
57| Claude Code max tool result size | 25,000 tokens (configurable via `MAX_MCP_OUTPUT_TOKENS`) |
58| Claude Code timeout | Configurable via `MCP_TOOL_TIMEOUT` |
59| Claude.ai/Desktop tool call timeout | 240 seconds (4 minutes) per tool call |
60| Transport protocol | Streamable HTTP (legacy HTTP+SSE being deprecated) |
65### Design within the size and timeout limits
6166 
62## Testing your server
67Keep tool results and tool call durations within these limits. They differ between the hosted surfaces and Claude Code.
6368 
641. Add directly to Claude via **Customize > Connectors**
652. Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) to validate auth flows
663. Add to Claude Code with `claude mcp add` and check `/mcp` for status. See the [Claude Code MCP quickstart](https://code.claude.com/docs/en/mcp-quickstart).
69| Limit | claude.ai and Desktop | Claude Code |
70| ------------------------ | ------------------------- | -------------------------------------------------------- |
71| Maximum tool result size | \~150,000 characters | 25,000 tokens, configurable with `MAX_MCP_OUTPUT_TOKENS` |
72| Tool call timeout | 240 seconds per tool call | Configurable with `MCP_TOOL_TIMEOUT` |
6773 
68## Related topics
74### Decide whether to add interactive UI
6975 
70<Columns cols={2}>
71 <Card title="MCP Overview" icon="plug" href="/docs/connectors/building/mcp">
72 Understanding the Model Context Protocol.
73 </Card>
76An MCP App is interactive UI that your MCP server renders inside a Claude conversation, such as an interactive chart or map. It's optional, and you build it as part of the same server. [Get started with MCP Apps](/docs/connectors/building/mcp-apps/getting-started) shows an example and how to build your own.
7477 
75 <Card title="Submit to Directory" icon="paper-plane" href="/docs/connectors/building/submission">
76 Review requirements and submit your connector.
77 </Card>
78## Test your server against Claude
7879 
79 <Card title="Test in Claude Code" icon="terminal" href="https://code.claude.com/docs/en/mcp">
80 Connect and debug your server with the Claude Code CLI.
81 </Card>
82</Columns>
80You test against the real Claude client, not a staging environment. [Test your connector](/docs/connectors/building/testing) covers adding the server to Claude as a custom connector, validating auth flows with the MCP Inspector, tunneling a local server, and preparing test credentials for review. To connect and debug from the Claude Code command line, see the [Claude Code MCP quickstart](https://code.claude.com/docs/en/mcp-quickstart).
81 
82## Decide how people get your server
83 
84People add your server to Claude as a connector in one of these ways:
85 
86* **As a custom connector**: a user or an organization Owner adds it by entering its URL, with no review by Anthropic
87* **From the directory**: Anthropic lists it in the directory after review, so people find it in Claude. [Publish to the directory](/docs/directory/publish) covers who can submit and what review involves
88* **Inside a plugin**: you bundle the server with the skills that teach Claude to use it, so people install both together. See [Plugin structure and testing](/docs/plugins/build)
89 
90Directory and custom connectors run on the same infrastructure. [Directory connectors vs custom connectors](/docs/connectors/building/directory-vs-custom) compares the two and explains when to offer both.
91 
92## Related resources
93 
94These resources cover the MCP protocol itself rather than Claude's client:
95 
96* **SDKs**: the [TypeScript](https://github.com/modelcontextprotocol/typescript-sdk) and [Python](https://github.com/modelcontextprotocol/python-sdk) SDKs contain server implementation examples
97* **Protocol specification**: [modelcontextprotocol.io](https://modelcontextprotocol.io)
98* **Authorization specification**: read the [authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization), especially the third-party service flows
99* **MCP Inspector**: validate auth flows outside Claude with the [inspector](https://modelcontextprotocol.io/docs/tools/inspector)
100 
101## Next steps
102 
103* [Authentication for connectors](/docs/connectors/building/authentication): pick an authentication type and meet Claude's OAuth requirements
104* [Test your connector](/docs/connectors/building/testing): add your server as a custom connector and debug connection failures
105* [Plugin structure and testing](/docs/plugins/build): bundle your connector with skills so people install both together
106* [Publish to the directory](/docs/directory/publish): submit your connector for review so people find it in claude.ai on the web, the desktop and mobile apps, and Cowork. Anyone on a paid Claude plan can submit, and on Team and Enterprise an Owner submits
83107 

connectors/building/lazy-authentication Changed · +198 / -124 lines

## See what the user experiences ## Build lazy authentication on your server ### Decide which tools need the user's account ### Answer a protected call with 401 before the MCP SDK runs #### Don't wrap the refusal in a 200 tool error ### Serve the discovery documents ### Identify Claude with a client ID metadata document #### Match loopback redirect URIs without the port ### Ask for more scope with 403 ### Allow for discovery caching ## Test the lazy-auth path ## Next steps ## Return 401, not a tool error ## Gate at the HTTP layer ## Serve the discovery documents ## OAuth discovery caching ## Step-up authorization ## Identify the client with CIMD ## Try it ## Adapting to your server

from line 2
22 
33> Let users call public tools immediately and defer OAuth until a protected tool is actually invoked.
44 
5Not every tool on an MCP server needs the user's identity. A product catalog can be browsed anonymously; an order history cannot. **Lazy authentication** (sometimes called *mixed auth*) lets a single server expose both: unauthenticated clients can connect, list tools, and call public ones, and the server only challenges for credentials when a protected tool is invoked. The challenge follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/latest/basic/authorization).
5A connector that uses OAuth sign-in normally asks each person to sign in as soon as they add it. With lazy authentication, sometimes called mixed auth, your MCP server lets Claude connect, list tools, and call the tools that don't need the person's account right away, and asks for sign-in only when Claude calls a tool that does. A product catalog can be browsed anonymously, for example, while an order history can't.
66 
7In Claude, the challenge surfaces as an inline **Connect** card in the conversation. The user authenticates in a popup, Claude retries the same tool call automatically with the new token, and the turn continues — no context is lost.
7When Claude reaches a protected tool, it shows the sign-in prompt inline in the conversation, and after the person signs in it retries the same tool call. The challenge your server sends to trigger that prompt follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/latest/basic/authorization).
88 
9This page is for developers whose MCP server has some tools that work without the user's identity. It walks through excerpts from an example Express server, a single `src/index.ts` file built on `@modelcontextprotocol/sdk` over Streamable HTTP, to show what your own server needs at each step. For the OAuth requirements every server must meet regardless of when it asks for sign-in, see [Authentication for connectors](/docs/connectors/building/authentication).
10 
11## See what the user experiences
12 
13A user of a lazy-auth connector goes through these steps:
14 
151. They add your connector and start using it. Claude calls your public tools with no sign-in prompt.
162. They ask for something that needs their account. Claude calls the protected tool, your server refuses it, and an inline **Connect** card appears in the conversation.
173. They click **Connect** and sign in to your service in a popup.
184. Claude retries the same tool call automatically with the new token, and the turn continues with no context lost.
19 
920<Note>
10 If the user's organization has [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth) configured for your connector, the `401` triggers a silent token exchange instead of the **Connect** card. The tool call is retried automatically and the user sees no prompt.
21 If the user's organization has [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth) configured for your connector, the same refusal triggers a silent token exchange instead of the **Connect** card. Claude retries the tool call automatically and the user sees no prompt.
1122</Note>
1223 
13The examples below are drawn from a single-file Express app using `@modelcontextprotocol/sdk` over Streamable HTTP.
24## Build lazy authentication on your server
1425 
15## Return 401, not a tool error
26Your MCP server and your authorization server each have a part in making lazy authentication work.
1627 
17The only detail that matters is **how** the server refuses an unauthenticated call to a protected tool.
28### Decide which tools need the user's account
1829 
19It must fail the **HTTP request** with `401 Unauthorized` and a [`WWW-Authenticate`](https://datatracker.ietf.org/doc/html/rfc6750#section-3) header:
30Split your tools into the ones anyone can call and the ones that act on the signed-in user's data. Claude can call the first group before sign-in, so keep it to tools that are safe without an identity, such as browsing a public catalog.
2031 
21```http theme={null}
22HTTP/1.1 401 Unauthorized
23WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://example.com/.well-known/oauth-protected-resource/mcp", scope="orders:read"
32In the example server, `list_products` is public and `get_my_orders` is protected. The protected names go in a `PROTECTED_TOOLS` set that the [HTTP handler's gate](#answer-a-protected-call-with-401-before-the-mcp-sdk-runs) checks. List your own protected tools there.
2433 
25{"error":"invalid_token","error_description":"Authentication required for this tool"}
26```
34### Answer a protected call with 401 before the MCP SDK runs
2735 
28The body is advisory; the `401` status and `WWW-Authenticate` header carry the protocol signal. The optional `scope` parameter tells Claude which scopes to request during authorization — include the minimum your protected tools need. If you omit it, Claude requests the scopes your protected resource metadata advertises in `scopes_supported` (plus `offline_access` if your authorization server metadata lists it), which can produce an over-broad consent prompt.
36Claude starts sign-in only when the HTTP request itself fails with `401 Unauthorized` and a [`WWW-Authenticate`](https://datatracker.ietf.org/doc/html/rfc6750#section-3) header. A tool handler can't produce that response: once the MCP SDK is running a tool, whatever the handler returns is wrapped in a `200`. The check has to run in your HTTP handler, on the parsed JSON-RPC body, before the request reaches the SDK.
2937 
30It must **not** return a successful HTTP response wrapping a tool error:
38On your server, when a request is a `tools/call` for a protected tool and carries no valid bearer token:
3139 
40* Respond with HTTP status `401`
41* Set a `WWW-Authenticate: Bearer` header whose `resource_metadata` parameter points at your [protected resource metadata](#serve-the-discovery-documents) document
42* Optionally add a `scope` parameter naming the minimum scopes your protected tools need
43* Return from the handler before calling the MCP SDK, and keep the check before `transport.handleRequest` even if your server uses stateful Streamable HTTP sessions
44 
45The response Claude expects looks like this:
46 
3247```http theme={null}
33HTTP/1.1 200 OK
48HTTP/1.1 401 Unauthorized
49WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required for this tool", resource_metadata="https://example.com/.well-known/oauth-protected-resource/mcp", scope="orders:read"
3450 
35{"jsonrpc":"2.0","result":{"isError":true,"content":[{"type":"text","text":"Please sign in"}]},"id":1}
51{"error":"invalid_token","error_description":"Authentication required for this tool"}
3652```
3753 
38<Warning>
39 A `200` with `isError: true` is an application-level tool failure. Claude passes the error text to the model as the tool result and moves on — there is no auth prompt. Only a transport-level `401` causes Claude to pause the call, run the OAuth flow, and retry. A `403` triggers re-authentication only when accompanied by `WWW-Authenticate: Bearer error="insufficient_scope"` for scope step-up; any other `403` is surfaced as a terminal error. If users are seeing "please sign in" text in the chat instead of a **Connect** button, the server is returning the wrong one.
40</Warning>
54The `401` status and the `WWW-Authenticate` header carry the signal, and the body is advisory. The `scope` parameter tells Claude which scopes to request during authorization. If you omit it, Claude requests every scope your protected resource metadata advertises in `scopes_supported`, plus `offline_access` if your authorization server metadata lists it, which can produce an over-broad consent prompt.
4155 
42The `resource_metadata` parameter in the `WWW-Authenticate` header points at the server's [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata (PRM), which in turn names the authorization server. That chain is how Claude discovers where to send the user without any of it being hard-coded in the client.
56In the example server's `POST /mcp` handler, the load-bearing lines are the `PROTECTED_TOOLS` set, the `WWW_AUTHENTICATE` header value with its `resource_metadata` URL, and the gate that sends the `401` and returns before the SDK transport is created. Those lines are highlighted:
4357 
44## Gate at the HTTP layer
58```ts src/index.ts {1,22-26,33-41} theme={null}
59const PROTECTED_TOOLS = new Set(["get_my_orders"]); // tools that need the user's account
4560 
46Because the refusal must be an HTTP status, the check has to happen **before** the JSON-RPC message reaches the MCP SDK. Once a tool handler is running, its return value is already destined to be wrapped in a `200` response.
47 
48The sample inspects the parsed JSON-RPC body in the Express handler and short-circuits if the request is a `tools/call` for a protected tool and no valid bearer is present:
49 
50```ts src/index.ts theme={null}
51const PROTECTED_TOOLS = new Set(["get_my_orders"]);
52 
5361function callsProtectedTool(body: unknown): boolean {
5462 const messages = Array.isArray(body) ? body : [body];
5563 for (const msg of messages) {
from line 75
6775 return false;
6876}
6977 
78// Claude starts sign-in only on HTTP 401 with this header.
79// resource_metadata is where Claude looks up your authorization server.
7080const WWW_AUTHENTICATE =
7181 `Bearer error="invalid_token", ` +
7282 `error_description="Authentication required for this tool", ` +
from line 87
7787 const token = extractBearer(req);
7888 const authed = isTokenValid(token);
7989 
80 // Lazy-auth gate: fail with 401 BEFORE the MCP layer sees the request.
81 // initialize, tools/list, and public tool calls fall through.
90 // The gate. initialize, tools/list, and public tool calls fall through.
8291 if (!authed && callsProtectedTool(req.body)) {
8392 res
8493 .status(401)
from line 96
8796 error: "invalid_token",
8897 error_description: "Authentication required for this tool",
8998 });
90 return;
99 return; // Return here, before the SDK, or the refusal turns into a 200 tool result.
91100 }
92101 
93 // Otherwise: stateless Streamable HTTP handling.
94102 const transport = new StreamableHTTPServerTransport({
95103 sessionIdGenerator: undefined,
96104 enableJsonResponse: true,
from line 122
114122});
115123```
116124 
117`initialize`, `tools/list`, and calls to `list_products` never hit the gate, so the connector is fully usable before sign-in. When the user already has a valid token, every request — public or protected — carries it and the gate is a no-op.
125`initialize`, `tools/list`, and calls to `list_products` never hit the gate, so the connector is fully usable before sign-in. When the user already has a valid token, every request carries it, public or protected, and the gate does nothing.
118126 
119The same pattern covers **scope upgrades**: if the bearer is valid but lacks a required scope, return `403 Forbidden` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"` and Claude prompts the user to re-consent. See [Step-up authorization](#step-up-authorization) below for what scopes Claude requests on re-consent and how the challenge is cached.
127The example server's `isTokenValid()` is a stub. In your server, replace it with real verification: either check the JWT signature, that `iss` matches your authorization server, that `aud` equals the `resource` value you advertise in your protected resource metadata, and `exp`, or use [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) token introspection against your identity provider.
120128 
121## Serve the discovery documents
129#### Don't wrap the refusal in a 200 tool error
122130 
123After a 401, Claude fetches the URL from `resource_metadata` to learn which authorization server to use:
131A successful HTTP response that wraps a tool error doesn't start sign-in. This is the shape to avoid:
124132 
125```ts src/index.ts theme={null}
133```http theme={null}
134HTTP/1.1 200 OK
135 
136{"jsonrpc":"2.0","result":{"isError":true,"content":[{"type":"text","text":"Please sign in"}]},"id":1}
137```
138 
139<Warning>
140 A `200` with `isError: true` is an application-level tool failure. Claude passes the error text to the model as the tool result and moves on, and there is no auth prompt. Only a transport-level `401` causes Claude to pause the call, run the OAuth flow, and retry. A `403` triggers re-authentication only when accompanied by `WWW-Authenticate: Bearer error="insufficient_scope"` for [scope step-up](#ask-for-more-scope-with-403), and any other `403` is surfaced as a terminal error. If users are seeing "please sign in" text in the chat instead of a **Connect** button, the server is returning the wrong one.
141</Warning>
142 
143### Serve the discovery documents
144 
145After the `401`, Claude needs to find out where to send the user to sign in, and nothing about your server is hard-coded in Claude. It fetches the `resource_metadata` URL from your `WWW-Authenticate` header, which serves your protected resource metadata: a small [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) JSON document that names your MCP endpoint as the `resource` and lists the authorization server that issues tokens for it. Claude then fetches that authorization server's [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) metadata to find its `/authorize` and `/token` endpoints.
146 
147On your server:
148 
149* Serve the protected resource metadata at `/.well-known/oauth-protected-resource/<your-mcp-path>`, the path-suffixed form clients try first when your MCP URL has a path such as `/mcp`, and at `/.well-known/oauth-protected-resource`
150* Set `resource` to your MCP endpoint URL and `authorization_servers` to your real issuer
151 
152In the example server, one function builds the protected resource metadata document and two routes serve it. The `resource` and `authorization_servers` fields and the path-suffixed route are highlighted:
153 
154```ts src/index.ts {3-4,15} theme={null}
126155function protectedResourceMetadata() {
127156 return {
128 resource: `${BASE_URL}/mcp`,
129 authorization_servers: [BASE_URL],
157 resource: `${BASE_URL}/mcp`, // must match the MCP URL the user adds in Claude
158 authorization_servers: [BASE_URL], // your issuer; the example is its own
130159 bearer_methods_supported: ["header"],
131160 };
132161}
from line 164
135164 res.json(protectedResourceMetadata());
136165});
137166 
138// Path-suffixed variant per RFC 9728 section 3.1 — clients try this first when
139// the resource URL has a path component (/mcp).
167// Path-suffixed variant per RFC 9728 section 3.1. Clients try this first
168// when the resource URL has a path component (/mcp).
140169app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
141170 res.json(protectedResourceMetadata());
142171});
143172```
144173 
145Claude then fetches the authorization server's [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) metadata to find the `/authorize` and `/token` endpoints.
174The example server acts as its own authorization server with stub `/authorize` and `/token` handlers. When you point `authorization_servers` at your real issuer, delete those stubs.
146175 
147## OAuth discovery caching
176### Identify Claude with a client ID metadata document
148177 
149Claude caches the discovery documents — your protected resource metadata and the authorization-server metadata it points to — **globally, keyed by URL**, with a staleness window of about five minutes by default. All Claude users connecting to the same server URL share a single cache entry, and distinct server URLs (for example, staging versus production) cache independently.
178Before your authorization server shows a consent screen, it has to know which app is asking. The example identifies Claude with a Client ID Metadata Document (CIMD), defined in [draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/).
150179 
151The refresh is lazy and best-effort: after you change `scopes_supported` (or any other discovery field), the new value is picked up by the first authorization that successfully re-runs discovery once the staleness window has elapsed, then propagates to everyone. There is no per-user expiry to wait for. If a refresh fails, Claude serves the stale entry and tries again on a later request, so an unreachable discovery endpoint doesn't immediately break existing connections — it just delays the change.
180With CIMD, Claude's `client_id` is an HTTPS URL, and your authorization server fetches it during `/authorize` to read Claude's registration details. That means you don't register Claude ahead of time, keep a client database, or run a Dynamic Client Registration (DCR) endpoint.
152181 
153## Step-up authorization
182Your `/authorize` endpoint should:
154183 
155The scope-upgrade case at the end of [Gate at the HTTP layer](#gate-at-the-http-layer) is the MCP specification's [Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow). When the bearer token is valid but missing a scope the requested tool needs, return `403 Forbidden` with a [`WWW-Authenticate`](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) challenge:
184* Fetch the `client_id` URL and check that the document's own `client_id` field equals that URL
185* Check the requested `redirect_uri` exactly against the document's `redirect_uris`, matching loopback URIs as [Match loopback redirect URIs without the port](#match-loopback-redirect-uris-without-the-port) describes. You can additionally require non-loopback `redirect_uris` to share the `client_id` URL's origin
186* On the consent screen, name the host of the `client_id` URL as the app asking for access, not the document's `client_name`, because the document is self-asserted
156187 
157```http theme={null}
158HTTP/1.1 403 Forbidden
159WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write"
160```
188Your authorization server advertises CIMD support in its own metadata, the [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) document Claude fetches after reading `authorization_servers`. Serve it at the RFC 8414 well-known path, which for the example's issuer is `/.well-known/oauth-authorization-server`. In the example, the two values Claude checks before it uses CIMD are highlighted:
161189 
162Claude prompts the user to re-authorize and, on consent, retries the same tool call with the new token.
163 
164**Which scopes Claude requests on re-authorization.** Claude unions the scopes named in your `403` challenge with the scope your server advertises during discovery (the `scope` parameter on your initial `401` `WWW-Authenticate` response, or your protected resource metadata's `scopes_supported` if you don't send one). Scopes the user picked up in an earlier step-up aren't reliably carried forward into the next one. To make sure the user keeps a permission they still need, follow the [MCP spec's recommended approach](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors) and include it in the `403` `scope` value alongside the newly required scopes — don't return only the single missing scope and depend on the client to remember the rest.
165 
166If your `403` carries `error="insufficient_scope"` but **omits the `scope` parameter**, Claude still recognizes step-up and runs its normal scope selection: the discovery-time `WWW-Authenticate` scope first, then your protected resource metadata's `scopes_supported`, then the authorization server metadata's `scopes_supported`.
167 
168<Note>
169 The `scope` value from your `403` is cached **per user, per server** for up to fifteen minutes and consumed by the next re-authorization that user starts against your server. The cache holds the most recent challenge — a new `403` overwrites the previous one — and is cleared once it's used. Combined with the [global discovery cache](#oauth-discovery-caching) above, a newly-added scope is available to step-up shortly after the discovery cache refreshes, typically within about five minutes of deploying the updated metadata.
170</Note>
171 
172## Identify the client with CIMD
173 
174The sample does **not** implement Dynamic Client Registration. Instead it advertises support for **Client ID Metadata Documents** ([draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)) in its authorization-server metadata:
175 
176```ts src/index.ts theme={null}
190```ts src/index.ts {9,11} theme={null}
177191function authorizationServerMetadata() {
178192 return {
179193 issuer: BASE_URL,
from line 196
182196 scopes_supported: ["profile", "orders:read"],
183197 response_types_supported: ["code"],
184198 grant_types_supported: ["authorization_code", "refresh_token"],
185 token_endpoint_auth_methods_supported: ["none"],
199 token_endpoint_auth_methods_supported: ["none"], // Claude's CIMD client is a public client
186200 code_challenge_methods_supported: ["S256"],
187 client_id_metadata_document_supported: true,
201 client_id_metadata_document_supported: true, // tells Claude it can send its client_id URL
188202 };
189203}
190204```
191205 
192With CIMD the `client_id` is itself an HTTPS URL that dereferences to the client's OAuth registration metadata. There is no per-client database and no `POST /register` round-trip: at `/authorize`, the server fetches the `client_id` URL, verifies the document is self-referential (its `client_id` field equals the URL it was served from), and checks the requested `redirect_uri` against the document's `redirect_uris`. Because the document is self-asserted, the consent screen must display the **host of the `client_id` URL** (not the `client_name` field) as the relying party, and the listed `redirect_uris` should be required to be same-origin with the `client_id` URL.
193 
194206<Note>
195 Claude selects CIMD only when the authorization-server metadata advertises **both** `client_id_metadata_document_supported: true` **and** `"none"` in `token_endpoint_auth_methods_supported`. The second is required because Claude's CIMD client authenticates as a public client (`token_endpoint_auth_method: "none"`), so the token endpoint must accept [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)-only requests without a client secret. If either property is missing, Claude falls back to looking for a `registration_endpoint`.
207 Claude selects CIMD only when the authorization-server metadata advertises both `client_id_metadata_document_supported: true` and `"none"` in `token_endpoint_auth_methods_supported`. The second is required because Claude's CIMD client authenticates as a public client with `token_endpoint_auth_method: "none"`, so the token endpoint must accept [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)-only requests without a client secret. If either property is missing, Claude falls back to looking for a `registration_endpoint`.
196208</Note>
197209 
198For native clients, compare loopback IP `redirect_uri` values (`http://127.0.0.1/…`, `http://[::1]/…`) with the **port ignored**, per [RFC 8252 section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3) — native apps bind an ephemeral port at runtime. RFC 8252 section 8.3 discourages `http://localhost/…`, but Claude Code declares it in its CIMD and binds an ephemeral port at runtime, so apply the same port-agnostic match to `localhost` for compatibility. The sample's `redirectUriAllowed()` helper shows the comparison.
210If you move to a real issuer, keep `client_id_metadata_document_supported: true` in that issuer's metadata if you want registration-free onboarding for Claude clients.
199211 
200## Try it
212#### Match loopback redirect URIs without the port
201213 
202<Steps>
203 <Step title="Run the server">
204 ```bash theme={null}
205 npm install
206 npm run build
207 npm start
208 ```
214Claude Code is a native client, and native apps bind an ephemeral port at runtime, so the `redirect_uri` Claude Code sends carries a port your `redirect_uris` check can't know in advance. When you compare a requested `redirect_uri` against the document:
209215 
210 The server listens on `http://localhost:3000/mcp`.
211 </Step>
216* Compare loopback IP values such as `http://127.0.0.1/…` and `http://[::1]/…` with the port ignored, per [RFC 8252 section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3)
217* Apply the same port-agnostic match to `http://localhost/…`. RFC 8252 section 8.3 discourages `localhost`, but Claude Code declares it in its CIMD, so accept it for compatibility
212218 
213 <Step title="Call a public tool without auth: 200">
219### Ask for more scope with 403
220 
221When a signed-in user calls a tool that needs a scope their token lacks, your server can ask Claude to re-authorize them instead of failing the call. This is the MCP specification's [Step-Up Authorization Flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow). From the same gate in your HTTP handler, return `403 Forbidden` with a [`WWW-Authenticate`](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) challenge that names the scopes:
222 
223```http theme={null}
224HTTP/1.1 403 Forbidden
225WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write"
226```
227 
228Claude prompts the user to re-authorize and, on consent, retries the same tool call with the new token. The scopes Claude requests on that re-authorization are the union of two sources:
229 
230* **Your `403` challenge**: the scopes named in its `scope` parameter. List every scope the user still needs alongside the newly required ones, not only the one that's missing, because scopes the user picked up in an earlier step-up aren't reliably carried forward into the next one. This is the [MCP spec's recommended approach](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors), and it avoids depending on the client to remember the rest
231* **Your discovery scope**: the `scope` parameter on your initial `401` `WWW-Authenticate` response, or your protected resource metadata's `scopes_supported` if you don't send one
232 
233If your `403` carries `error="insufficient_scope"` but omits the `scope` parameter, Claude still recognizes step-up and runs its normal scope selection: the discovery-time `WWW-Authenticate` scope first, then your protected resource metadata's `scopes_supported`, then the authorization server metadata's `scopes_supported`.
234 
235### Allow for discovery caching
236 
237After you change `scopes_supported` or any other discovery field, allow about five minutes before Claude uses the new values. Claude caches your protected resource metadata and the authorization-server metadata it points to globally, keyed by URL, with a staleness window of about five minutes by default.
238 
239How the cache is keyed and refreshed decides when a change reaches your users:
240 
241* **One cache entry per server URL**: all Claude users connecting to the same server URL share a single entry, and distinct URLs, such as staging versus production, cache independently. There is no per-user expiry to wait for
242* **Lazy refresh**: the first authorization that successfully re-runs discovery after the window has elapsed picks up the new value, and it then propagates to everyone
243* **Stale on failure**: if a refresh fails, Claude serves the stale entry and tries again on a later request, so an unreachable discovery endpoint doesn't immediately break existing connections. It only delays the change
244 
245## Test the lazy-auth path
246 
247You can confirm the public and protected paths with `curl` and Claude Code against your own server before connecting it to Claude as a custom connector, then check the sign-in prompt in a conversation. The commands below use `http://localhost:3000/mcp` as the server URL and the example's tool names; substitute your own.
248 
249<Steps>
250 <Step title="Call a public tool without a token">
251 In a terminal, send a `tools/call` for a public tool with no `Authorization` header, with `-i` so the status line prints:
252 
214253 ```bash theme={null}
215 curl -s http://localhost:3000/mcp \
254 curl -si http://localhost:3000/mcp \
216255 -H 'Content-Type: application/json' \
217256 -H 'Accept: application/json, text/event-stream' \
218257 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_products","arguments":{}}}'
219258 ```
259 
260 The server answers `200 OK` with the JSON-RPC tool result and no `WWW-Authenticate` header.
220261 </Step>
221262 
222 <Step title="Call a protected tool without auth: 401">
263 <Step title="Call a protected tool without a token">
264 Send the same request for a protected tool:
265 
223266 ```bash theme={null}
224267 curl -si http://localhost:3000/mcp \
225268 -H 'Content-Type: application/json' \
from line 270
227270 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_my_orders","arguments":{}}}'
228271 ```
229272 
230 Note the `WWW-Authenticate` header in the response.
273 The server answers `401 Unauthorized` with a `WWW-Authenticate` header carrying `resource_metadata`. If you see `200` with `isError: true` instead, your server is returning a tool error rather than failing the HTTP request.
231274 </Step>
232275 
233 <Step title="Add it as a custom connector in Claude">
234 Claude reaches custom connectors from Anthropic's infrastructure, so `localhost` is not reachable directly. Expose the server over a public HTTPS tunnel (for example, `cloudflared tunnel --url http://localhost:3000` or `ngrok http 3000`), then in **Customize > Connectors**, select **Add custom connector** and enter the tunnel's `/mcp` URL. See [Testing your connector](/docs/connectors/building/testing) for details.
276 <Step title="Check both paths from Claude Code">
277 If you have [Claude Code](https://code.claude.com/docs/en/setup) installed and signed in, you can see how a Claude client treats each path before you set up a tunnel, because Claude Code connects to a `localhost` server directly. In your terminal, from your server's project folder, add the server:
235278 
236 Ask Claude to list products (no prompt), then ask for your orders — the inline **Connect** card appears, and after authenticating the same call completes.
279 ```bash theme={null}
280 claude mcp add --transport http authtest http://localhost:3000/mcp
281 ```
282 
283 Then check the connection:
284 
285 ```bash theme={null}
286 claude mcp list
287 ```
288 
289 The list shows `authtest: http://localhost:3000/mcp (HTTP) - ✔ Connected` with no sign-in, because connecting and listing tools never reach the gate. Next, have Claude call the public tool with [`claude -p`](https://code.claude.com/docs/en/headless), which sends one prompt and prints the answer:
290 
291 ```bash theme={null}
292 claude -p "List the products from the authtest server." --allowedTools mcp__authtest__list_products
293 ```
294 
295 Claude reports the products and no sign-in prompt appears. Run the same command asking for your orders with `--allowedTools mcp__authtest__get_my_orders`. The tool result Claude receives is `MCP server "authtest" needs you to sign in again (run /mcp to re-authenticate)` rather than order data.
296 
297 To sign in from Claude Code, run `/mcp` in an interactive session as [Authenticate with remote MCP servers](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers) describes. When you're done testing, run `claude mcp remove authtest`.
237298 </Step>
238</Steps>
239299 
240The sample's README includes a longer `curl` walkthrough that drives the stub `/authorize` and `/token` endpoints directly.
300 <Step title="Add the server to Claude as a custom connector">
301 Claude reaches custom connectors from Anthropic's infrastructure, so `localhost` isn't reachable directly. To add a local server:
241302 
242## Adapting to your server
303 1. Expose the server over a public HTTPS tunnel, such as `cloudflared tunnel --url http://localhost:3000` or `ngrok http 3000`. Keep the tunnel up only while you test, because it exposes your local server publicly and the example's `/authorize`, `/token`, and `isTokenValid()` are stubs that treat anyone as signed in.
304 2. In Claude, go to [**Customize > Connectors**](https://claude.ai/customize/connectors) and select **Add custom connector**.
305 3. Enter the tunnel's `/mcp` URL.
243306 
244* List your protected tools in `PROTECTED_TOOLS`.
245* Replace `isTokenValid()` with real verification: JWT signature, `iss` matches your authorization server, `aud` equals the `resource` value you advertise in the PRM, and `exp`; or [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) token introspection against your IdP.
246* Point `authorization_servers` in the PRM at your real issuer and delete the stub `/authorize` and `/token` handlers. Keep `client_id_metadata_document_supported: true` in your issuer's metadata if you want registration-free onboarding for Claude clients.
247* If your server uses stateful Streamable HTTP sessions, the gate still belongs in the `POST /mcp` handler, before `transport.handleRequest`.
307 See [Test a local server](/docs/connectors/building/testing#test-a-local-server) for details. If your app comes from the SDK's `createMcpExpressApp()`, that section also shows the `allowedHosts` option you need before requests through the tunnel succeed.
308 </Step>
309 
310 <Step title="Try both paths in a conversation">
311 1. Ask Claude to list products. No sign-in prompt appears.
312 2. Ask for your orders. The inline **Connect** card appears, and after you authenticate the same call completes.
313 </Step>
314</Steps>
315 
316## Next steps
317 
318* [Authentication for connectors](/docs/connectors/building/authentication): check the OAuth requirements your authorization server must meet, including redirect URIs and token refresh
319* [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth): let organizations with SSO answer the same `401` with a silent token exchange
320* [Test your connector](/docs/connectors/building/testing): expose the server through a tunnel and add it as a custom connector
321* [Troubleshoot your connector](/docs/connectors/building/troubleshooting): diagnose discovery and authorization failures
248322 

connectors/building/managing-your-listing Changed · +39 / -33 lines

# Manage your directory listing ### Review what the metrics cover ## Next steps # Managing your directory listing ### What the metrics cover

from line 1
1# Managing your directory listing
1# Manage your directory listing
22 
3> Track submissions, monitor server health and usage metrics, and edit your Connectors Directory listing
3> Track submissions, monitor server health and usage metrics, and edit your directory listing
44 
5Organizations that submit to the [Connectors Directory](/docs/connectors/directory) get a submissions dashboard in Claude.ai at [Organization settings > Directory](https://claude.ai/admin-settings/directory/submissions). Use it to track submissions through review, monitor your published server's health and usage, and edit your listing.
5Organizations that submit a connector to the [directory](/docs/connectors/directory) get a submissions dashboard in the developer portal at [claude.ai/directory/manage](https://claude.ai/directory/manage). This page is for connector authors who have already [submitted a connector](/docs/connectors/building/submission). Use the dashboard to [track a submission through review](#track-submission-status), [monitor your published server's health and usage](#server-health-and-usage-metrics), and [edit your listing](#edit-your-listing).
66 
77<Note>
8 The dashboard covers directory-listed remote MCP servers only. Custom connectors and local servers (desktop extensions) don't appear here, and the dashboard shows only your own organization's submissions.
8 This page covers directory-listed remote MCP servers. The dashboard shows only your own organization's submissions. Custom connectors and local servers packaged as desktop extensions don't appear here.
99</Note>
1010 
1111## Access the dashboard
1212 
13The dashboard is part of your organization's admin settings, so you need:
13The dashboard uses the same access as submitting. [Who can submit to the directory](/docs/directory/publish#confirm-you-can-submit-to-the-directory) lists the plan and role requirements. That access covers everything on this page: viewing submissions, metrics, and reviewer feedback, and editing and submitting listings.
1414 
15* **A Team or Enterprise organization**
16* **Directory management access.** By default, only organization Owners and Primary owners have it. On Enterprise, an Owner can delegate access through a custom role with the **Directory** or **Libraries** permission; see [Before you start](/docs/connectors/building/submission#before-you-start) for the steps. Team plans don't have custom roles, so on Team this stays with Owners.
17 
18The same access covers everything on this page: viewing submissions, metrics, and reviewer feedback, and editing and submitting listings.
19 
2015## Track submission status
2116 
22The dashboard lists each of your organization's submissions with its current status. Open a submission to see its full details and any reviewer feedback. When reviewers request changes, their feedback appears on the submission's detail page; address it and resubmit from the same page.
17The dashboard lists each of your organization's submissions with its current status. [Track your submission](/docs/directory/submission-status#mcp-connector-statuses) says what each status means and who acts next. Open a submission to see its full details and any reviewer feedback. When reviewers request changes, their feedback appears on the submission's detail page, where you address it and resubmit.
2318 
19A published connector is listed with the **Community** label by default. [Connector verification](/docs/connectors/verification#list-your-own-connector) explains how a listing becomes Verified.
20 
2421## Server health and usage metrics
2522 
23Once your server is published, its **Overview** and **Usage** tabs in the developer portal show a health badge, topline usage numbers, an error breakdown, and usage by product and by tool.
24 
2625<Note>
2726 Metrics are in beta. They're computed daily from directory usage and can lag by up to 24 hours. Time windows with fewer than 5 calls, per-tool rows with fewer than 5 calls, and per-product rows with fewer than 50 calls are omitted.
2827</Note>
2928 
30Once your server is published, its detail page shows health and usage data.
29### Review what the metrics cover
3130 
32### What the metrics cover
31All of the metrics on this page measure traffic from Claude: claude.ai, Claude Desktop, Claude Code, and other Claude surfaces. Connections that people make to your server from other MCP clients, and local servers that run on a user's own machine, aren't visible to Anthropic and aren't counted. Your own server logs can therefore show activity that this page doesn't.
3332 
34All of the metrics on this page measure traffic from Claude: Claude.ai, Claude Desktop, Claude Code, and other Claude surfaces. Connections that people make to your server from other MCP clients, and local servers that run on a user's own machine, aren't visible to Anthropic and aren't counted. Your own server logs can therefore show activity that this page doesn't.
33Tool call totals, error rates, and latency are measured at Anthropic's HTTP connector proxy. The **Used a tool** count and directory rank are measured from the MCP message stream, which covers both the HTTP and legacy WebSocket transports. If users reach your server only over the legacy WebSocket transport, you'll still see the **Used a tool** count and a directory rank, but no tool call totals, error rates, or latency data.
3534 
36Tool call totals, error rates, and latency are measured at Anthropic's HTTP connector proxy. Tool call users and directory rank are measured from the MCP message stream, which covers both the HTTP and legacy WebSocket transports. If users reach your server only over the legacy WebSocket transport, you'll still see tool call users and a directory rank, but no tool call totals, error rates, or latency data.
37 
3835### Health
3936 
40The health badge summarizes your server's recent reliability:
37The health badge summarizes your server's recent reliability as one of these statuses:
4138 
42| Status | Meaning |
43| --------------- | ------------------------------------------------------------------------------------------------------------ |
44| **Healthy** | The 30-day disconnect rate is at or below 5% |
45| **Degraded** | The 30-day disconnect rate is above 5% |
46| **Collecting…** | The server is published, but there isn't enough data yet to compute a disconnect rate (metrics update daily) |
47| **Not live** | The server isn't published yet; health appears after publication |
39| Status | Meaning |
40| ------------------- | -------------------------------------------------------------------- |
41| **Healthy** | Request errors are 2% or less of tool calls in the last 30 days |
42| **Worth a look** | Request errors are above 2% |
43| **Degraded** | Request errors are above 5% |
44| **Not enough data** | Request errors haven't been measured yet; metrics update daily |
45| **Not live** | The server isn't published yet, and health appears after publication |
4846 
49The disconnect rate is measured against every distinct Claude account that sent your server any MCP message in the last 30 days, including connection attempts that never completed: it is the share of those accounts that chose to disconnect your connector during those 30 days.
47Request errors include tool calls rejected for authentication problems and exclude errors a tool returns in its own result.
5048 
5149### Topline metrics
5250 
53* **Directory rank**: your position among published directory servers, highest first, ranked by the number of distinct Claude accounts that sent your server any MCP message in the last 30 days. Every MCP message counts toward the ranking, including `initialize` and `tools/list`, and it includes accounts whose connection attempt never finished authenticating — a broader population than the Tool call users card below. A "Trending" tag marks servers in the top 10 by recent growth in that same message-based count.
54* **Tool call users (30d)**: the number of distinct Claude accounts that made at least one tool call (`tools/call`) to your server in the last 30 days. Accounts that only attempted to connect, or connected and browsed your tools without calling one, aren't counted. This card was previously labeled "Active users (30d)" and counted every MCP message, including handshakes from connection attempts that never completed; the renamed metric counts only accounts that actually used your tools, so it is usually much smaller than the number the old card showed.
55* **Tool calls (30d)**: the number of `tools/call` requests Anthropic received for your server in the last 30 days. Protocol messages such as `initialize` and `tools/list` aren't counted here. This is a count of requests, not of users, so retries are included, as are requests that were turned away for authentication problems.
56* **Error rate (30d)**: of the tool calls above, the fraction that either failed at the request level (for example, with a 5xx response or a timeout) or returned an MCP tool result with `isError: true`. Tool-call requests that were turned away for authentication problems aren't counted as errors, but they are included in the total number of tool calls that the rate is measured against. Shown with the most common error types.
51The topline metric cards summarize your server's reach, usage, and reliability over the last 30 days:
5752 
53* **Directory rank**: your position among published directory servers, highest first, ranked by the number of distinct Claude accounts that sent your server any MCP message in the last 30 days. Every MCP message counts toward the ranking, including `initialize` and `tools/list`, and it includes accounts whose connection attempt never finished authenticating, which is a broader population than the **Used a tool** card counts. A **Trending** tag marks servers in the top 10 by recent growth in that same message-based count
54* **Used a tool**: the number of distinct Claude accounts that made at least one tool call (`tools/call`) to your server in the last 30 days. Accounts that only attempted to connect, or connected and browsed your tools without calling one, aren't counted. The renamed metric counts only accounts that actually used your tools, so it's usually much smaller than the number the old card showed
55* **Tool calls (30d)**: the number of `tools/call` requests Anthropic received for your server in the last 30 days. Protocol messages such as `initialize` and `tools/list` aren't counted here. This is a count of requests, not of users, so retries are included, as are requests that were turned away for authentication problems
56* **Error rate (30d)**: of those tool calls, the fraction that either failed at the request level, for example with a 5xx response or a timeout, or returned an MCP tool result with `isError: true`. Tool-call requests that were turned away for authentication problems aren't counted as errors, but they are included in the total number of tool calls that the rate is measured against. Shown with the most common error types
57 
5858### Error breakdown
5959 
6060A table breaks errors out over 1-day, 7-day, and 30-day windows: total calls, overall error rate, tool versus request error rates, HTTP 4xx and 5xx rates, and the top error types in each window.
from line 61
6161 
6262### Usage by product
6363 
64A per-product table shows 7-day calls, error rate, and p50/p95/p99 latency, broken down by the Claude product the calls came from, such as Claude.ai, Claude Desktop, Claude Code, and Cowork. Only a fixed set of Claude surfaces is shown, and Anthropic's own internal monitoring traffic is excluded.
64A per-product table shows 7-day calls, error rate, and p50/p95/p99 latency, broken down by the Claude product the calls came from, such as claude.ai, Claude Desktop, Claude Code, and Cowork. Only a fixed set of Claude surfaces is shown, and Anthropic's own internal monitoring traffic is excluded.
6565 
66Because this table covers a shorter window, drops low-volume rows, shows only a fixed set of Claude surfaces, and excludes internal monitoring traffic, its call counts won't add up to the 30-day tool call total above. That's expected.
66Because the per-product table covers a shorter window, drops low-volume rows, shows only a fixed set of Claude surfaces, and excludes internal monitoring traffic, its call counts won't add up to the 30-day **Tool calls** total.
6767 
6868A high error rate on a single product may reflect a client-side issue on Anthropic's end rather than a problem with your server.
6969 
from line 73
7373 
7474## Edit your listing
7575 
76Open your submission's detail page to edit the listing. You can change directly:
76You edit a published listing from its submission detail page in the dashboard. You can change these fields directly:
7777 
78* **Listing metadata**: tagline, description, categories, documentation and privacy policy links, support contact, and icon
78* **Listing metadata**: tagline, description, categories, tool and prompt names, documentation and privacy policy links, support contact, and icon
7979* **Company details**: company name and website
8080* **Display name**: editable, but changing the name of a published server affects existing users and requires re-review
8181 
8282Save your edits as you go, then submit them for review. Submitted changes show as pending until a reviewer approves them, and you can discard pending changes before they're approved.
8383 
84The URL slug is locked: it's permanent after publication, since it determines your listing URL.
84The URL slug is locked. It's permanent after publication because it determines your listing URL.
8585 
8686For other edits or escalations, email `[email protected]`.
87 
88## Next steps
89 
90* [After publishing](/docs/connectors/building/after-publishing): release updates to your server, and delist
91* [Connector verification](/docs/connectors/verification#list-your-own-connector): see how a Community listing becomes Verified
92* [Troubleshoot your connector](/docs/connectors/building/troubleshooting): diagnose the errors behind a degraded health badge or high error rate
8793 

connectors/building/mcp Changed · +41 / -38 lines

## Understand what MCP provides ## Understand how MCP servers work ### Local and remote servers ### Tools, resources, and prompts ## Build with MCP ## Next steps ## What is MCP? ## How MCP works ### Local vs remote servers ### Key components ## Building with MCP ### For developers ### Submitting to directory ## Related topics

from line 1
11# Model Context Protocol (MCP)
22 
3> Understanding the open standard powering Claude's connectors
3> Get oriented to the Model Context Protocol, the open standard behind Claude's connectors: local and remote servers, tools, resources, and prompts.
44 
5The Model Context Protocol (MCP) is an open standard created by Anthropic for AI applications to connect with tools and data sources.
5The Model Context Protocol (MCP) is an open standard created by Anthropic for AI applications to connect with tools and data sources. When you build an MCP server and someone adds it to Claude, they see it as a [connector](/docs/connectors/getting-started).
66 
7## What is MCP?
7This page is a short orientation for developers who are new to MCP. The [MCP documentation](https://modelcontextprotocol.io/docs) is the source of truth for building MCP servers, and [Build an MCP server for Claude](/docs/connectors/building/index) lists what Claude's client supports once you're ready to build.
88 
9MCP provides a standardized way for AI assistants like Claude to:
9## Understand what MCP provides
1010 
11MCP gives AI assistants like Claude a standardized way to do the following:
12 
1113* Connect to external tools and services
1214* Access data from various sources
1315* Perform actions on behalf of users
1416* Maintain security and user control
1517 
16## How MCP works
18## Understand how MCP servers work
1719 
18### Local vs remote servers
20An MCP server runs either on the user's device or on the internet, and exposes tools, resources, and prompts to Claude.
1921 
20| Type | Description | Use Case |
21| -------------- | ---------------------- | --------------------------------- |
22| **Local MCP** | Runs on your device | Desktop integrations, local tools |
23| **Remote MCP** | Hosted on the internet | Web services, cloud applications |
22### Local and remote servers
2423 
25### Key components
24Where the server runs determines which integrations it suits.
2625 
27* **Tools**: Actions Claude can perform (search, create, modify)
28* **Resources**: Data Claude can access (files, documents, records)
29* **Prompts**: Predefined interactions for specific tasks
26| Type | Description | Use case |
27| ---------- | ------------------------- | --------------------------------- |
28| Local MCP | Runs on the user's device | Desktop integrations, local tools |
29| Remote MCP | Hosted on the internet | Web services, cloud applications |
3030 
31### Tools, resources, and prompts
32 
33A server exposes its capabilities to Claude as tools, resources, and prompts:
34 
35* **Tools**: actions Claude can perform, such as search, create, and modify
36* **Resources**: data Claude can access, such as files, documents, and records
37* **Prompts**: predefined interactions for specific tasks
38 
3139## Security model
3240 
41Users stay in control of each connector, and every tool declares whether it can change data.
42 
3343### User control
3444 
35* You authenticate each connector individually
36* Permissions mirror your access on the external service
37* You can disconnect at any time
45Each person who uses your connector keeps these controls:
3846 
47* They authenticate each connector individually
48* Their permissions mirror their access on the external service
49* They can disconnect at any time
50 
3951### Tool hints
4052 
41All MCP tools must declare:
53All MCP tools must declare both of these annotations:
4254 
43* [`readOnlyHint`](https://modelcontextprotocol.io/specification/latest/schema#toolannotations-readonlyhint): Tool only reads data
44* [`destructiveHint`](https://modelcontextprotocol.io/specification/latest/schema#toolannotations-destructivehint): Tool can modify or delete data
55* [`readOnlyHint`](https://modelcontextprotocol.io/specification/latest/schema#toolannotations-readonlyhint): the tool only reads data
56* [`destructiveHint`](https://modelcontextprotocol.io/specification/latest/schema#toolannotations-destructivehint): the tool can modify or delete data
4557 
46This helps Claude and users understand what actions are possible.
58Claude and users read these hints to understand what actions a tool can take.
4759 
48## Building with MCP
60## Build with MCP
4961 
50The [MCP documentation](https://modelcontextprotocol.io/docs) is the source of truth for building MCP servers.
62The [MCP documentation](https://modelcontextprotocol.io/docs) is the source of truth for building MCP servers. Start from these resources:
5163 
52### For developers
53 
5464* Open specification at [modelcontextprotocol.io](https://modelcontextprotocol.io)
55* [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) and [Python SDK](https://github.com/modelcontextprotocol/python-sdk) available
65* [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) and [Python SDK](https://github.com/modelcontextprotocol/python-sdk)
5666* Cloudflare hosting support with OAuth
5767 
58### Submitting to directory
59 
6068Organizations can [submit MCP servers](/docs/connectors/building/submission) to the Connectors Directory for broader availability.
6169 
62## Related topics
70## Next steps
6371 
64<Columns cols={2}>
65 <Card title="MCP in Claude Code" icon="terminal" href="https://code.claude.com/docs/en/mcp">
66 Connect MCP servers to Claude Code from the command line.
67 </Card>
68 
69 <Card title="Submit to Directory" icon="paper-plane" href="/docs/connectors/building/submission">
70 Review requirements and submit your connector.
71 </Card>
72</Columns>
72* [Build an MCP server for Claude](/docs/connectors/building/index): the transports, authentication, protocol features, and limits Claude's client supports
73* [Authentication for connectors](/docs/connectors/building/authentication): OAuth requirements and supported auth types
74* [MCP in Claude Code](https://code.claude.com/docs/en/mcp): connect MCP servers to Claude Code from the command line
75* [Submitting to the Connectors Directory](/docs/connectors/building/submission): review requirements and submit your connector
7376 

connectors/building/mcp-apps/cross-compatibility Page removed · 35 lines, page removed

# Building cross-platform MCP Apps ## How it works ### Server side ### Client side ## Platform differences ### Domain handling

The page is gone upstream. What it last said is kept here.

connectors/building/mcp-apps/design-guidelines Changed · +155 / -125 lines

## Design for the conversation ## Choose what to build as an MCP App #### Safe areas #### Borderless inline content ### Declare supported display modes ### Color ### Typography ### Borders ### Icons ### Spacing ### Accessibility ### Decide between app and chat interactions ### Reveal complexity progressively ### Prefer visible controls over hidden menus ### Color tokens ### Typography tokens ### Radius tokens ### Border width tokens ### Shadow tokens ## Related resources ## Overview ## What makes a good MCP App ### Display modes ### App vs. chat interactions ### Start simple ### Visible controls over hidden menus ## Accessibility

from line 1
11# Design guidelines
22 
3> Visual and interaction design guidelines for MCP Apps in Claude
3> Design MCP Apps that feel native to Claude: display modes, mobile layout, visual style, interaction patterns, and the host's style variables.
44 
5## Overview
5MCP Apps are interactive interfaces that appear within Claude's conversational flow. These guidelines are for developers designing an MCP App's UI.
66 
7MCP Apps are interactive interfaces that appear within Claude's conversational flow. Think of them as natural extensions of the conversation, not separate apps that happen to appear alongside it. Your app inherits conversational context and helps users accomplish meaningful tasks without breaking flow.
7<Note>
8 If you haven't built and connected an MCP App yet, see [Get started with MCP Apps](/docs/connectors/building/mcp-apps/getting-started).
9</Note>
810 
9**Core principles:**
11Use the guidelines to [pick a display mode](#display-modes), [adapt to mobile](#mobile-guidelines), match Claude's [visual design](#visual-design) with the host's [style variables](#style-variables), and decide which [interactions](#interaction-patterns) belong in your app and which belong in chat.
1012 
11* **Conversational.** Fit naturally into dialogue. Don't force users to learn new interaction patterns.
12* **Contextual.** Use conversation history to inform what you display and when.
13* **Integrated.** Inherit styling and conventions from the containing environment.
14* **Adaptive.** Handle variable sizing, mobile viewports, and diverse accessibility needs gracefully.
15 
1613<Tip>
17 See our [Figma UI kit](https://www.figma.com/community/file/1597641111449594397/mcp-apps-for-claude) for components and patterns to help you get started.
14 The [Figma UI kit](https://www.figma.com/community/file/1597641111449594397/mcp-apps-for-claude) has components and patterns to start from.
1815</Tip>
1916 
20## What makes a good MCP App
17## Design for the conversation
2118 
22**Good candidates:**
19Design your app as an extension of the conversation rather than a separate app that appears alongside it. Your app inherits conversational context and helps users accomplish meaningful tasks without breaking flow. These principles follow from that:
2320 
24* Tasks that fit naturally into conversation like data analysis, document review, or project coordination
25* Communication and collaboration context like message search results, conversation threads, or team member profiles
26* Tasks with a clear start and end like booking, ordering or scheduling
21* **Conversational**: fit naturally into dialogue, and don't force users to learn new interaction patterns
22* **Contextual**: use conversation history to inform what you display and when
23* **Integrated**: inherit styling and conventions from the containing environment
24* **Adaptive**: handle variable sizing, mobile viewports, and diverse accessibility needs gracefully
25 
26## Choose what to build as an MCP App
27 
28An MCP App works best for a task the user can finish inside the conversation. Good candidates include:
29 
30* Tasks that fit naturally into conversation, like data analysis, document review, or project coordination
31* Communication and collaboration context, like message search results, conversation threads, or team member profiles
32* Tasks with a clear start and end, like booking, ordering, or scheduling
2733* Information users can act on immediately
2834* Functionality that extends Claude's capabilities meaningfully
2935 
30**Patterns to avoid:**
36Avoid these patterns:
3137 
3238* Long-form or static content better suited for external viewing
3339* Complex multi-step workflows that exceed the display mode's scope
34* Deep navigation (no drill-ins, breadcrumbs, or multiple views)
35* Nested scrolling (inline cards should auto-fit content height)
36* Menus and popovers (dropdowns, context menus, and popover panels can get clipped by container boundaries or create z-index conflicts with the host UI — prefer visible controls like segmented buttons, toggles, or inline options)
37* Chat inputs or conversational UI (don't replicate Claude's features)
40* Deep navigation such as drill-ins, breadcrumbs, or multiple views
41* Nested scrolling, because inline cards should auto-fit content height
42* Menus and popovers: dropdowns, context menus, and popover panels can get clipped by container boundaries or create z-index conflicts with the host UI, so prefer visible controls like segmented buttons, toggles, or inline options
43* Chat inputs or conversational UI that replicate Claude's own features
3844 
3945## Display modes
4046 
47An MCP App appears in the conversation as an inline card, an inline carousel, or a full screen view. Each mode suits different content and carries its own constraints on desktop and on mobile.
48 
4149### Inline card
4250 
43Compact components embedded directly in conversation. Good for summaries, confirmations, and quick actions. Keep them focused.
51An inline card is a compact component embedded directly in the conversation. Keep it focused. Use an inline card for:
4452 
45**When to use:**
46 
4753* Status updates and confirmations
4854* Simple data displays or selections
4955* Brief summaries with optional expansion
from line 59
5359 
5460<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/inline-card-2.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=344f114b1012838b4805b48d496201c0" alt="Inline card example showing a data display" width="1999" height="1423" data-path="images/mcp-apps/inline-card-2.png" />
5561 
56**Constraints:**
62Inline cards have these constraints:
5763 
58* Height: auto-fits to content (no nested scrolling)
59* Max actions: 2, placed at the bottom of the card
60* Max data points: 4-5
64* Height auto-fits to content, with no nested scrolling
65* At most 2 actions, placed at the bottom of the card
66* At most 4-5 data points
6167* No drill-ins, breadcrumbs, or multiple views
62* No menus or popovers — use visible controls instead
68* No menus or popovers, only visible controls
6369 
64**On mobile:** Inline cards render full-width within the conversation. Ensure all tap targets are at least 44pt. Content should adapt to narrower viewports without horizontal scrolling.
70On mobile, inline cards render full-width within the conversation. Make every tap target at least 44pt, and adapt content to narrower viewports without horizontal scrolling.
6571 
6672<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/inline-card-mobile.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=3d2afa48da4f1d31cce9c0bb91a18807" alt="Inline card mobile examples" width="1999" height="838" data-path="images/mcp-apps/inline-card-mobile.png" />
6773 
6874### Inline carousel
6975 
70Side-by-side items for browsing options. Users swipe or scroll horizontally to explore.
76An inline carousel shows items side by side for browsing options. Users swipe or scroll horizontally to explore. Use an inline carousel for:
7177 
72**When to use:**
73 
7478* Product listings or search results
7579* Location or venue options
7680* Media galleries
from line 82
7882 
7983<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/inline-carousel-1.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=ef6b5615726ad25708ebdedf586226ae" alt="Inline carousel example showing browsable items" width="1999" height="1423" data-path="images/mcp-apps/inline-carousel-1.png" />
8084 
81**Constraints:**
85Inline carousels have these constraints:
8286 
8387* 3-8 items for scannability
84* Each card: image + title + metadata (max 3 lines) + optional CTA
88* Each card has an image, a title, up to 3 lines of metadata, and an optional CTA
8589* 1 optional CTA per card
86* Maintain consistent card dimensions within a carousel
87* Cards should have consistent visual hierarchy
90* Consistent card dimensions within a carousel
91* Consistent visual hierarchy across cards
8892 
89**On mobile:** Carousel cards are optimized for horizontal swipe. Design for thumb reach — keep primary actions in the lower portion of cards. Peek the next card to signal scrollability.
93On mobile, carousel cards are optimized for horizontal swipe. Design for thumb reach by keeping primary actions in the lower portion of cards, and let the next card peek into view to signal that the row scrolls.
9094 
9195<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/inline-carousel-mobile.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=2bf64edd4fdefe8a37188ca4f2f32f4e" alt="Inline carousel mobile examples" width="1999" height="1022" data-path="images/mcp-apps/inline-carousel-mobile.png" />
9296 
9397### Full screen
9498 
95Immersive interfaces for complex interactions. The conversation composer remains available so users can continue talking to your app through Claude. Apps provide their own fullscreen button. A close button appears in the native header bar when in fullscreen mode. In fullscreen mode, avoid the use of floating panels. Use collapsible sidebars, tabs or pagination to disclose details.
99Full screen mode gives complex interactions an immersive interface. The conversation composer remains available, so users can continue talking to your app through Claude. Use full screen for:
96100 
97**When to use:**
98 
99101* Data visualizations and dashboards
100102* Detailed analysis tools
101103* Document editing
from line 108
106108 
107109<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/fullscreen-2.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=8523c5ae46ab7492586044ff5fabea32" alt="Full screen mode with data visualization" width="1999" height="1423" data-path="images/mcp-apps/fullscreen-2.png" />
108110 
109**Constraints:**
111Full screen mode has these constraints:
110112 
111* Your app provides its own fullscreen button; a close button appears in the native header bar
112* The composer is always visible — design your UX to work with it
113* No floating panels — use collapsible sidebars, tabs, or pagination to disclose details
114* Chat sheet maintains conversational context
113* Your app provides its own fullscreen button, and a close button appears in the native header bar
114* The composer is always visible, so design your UX to work with it
115* No floating panels, so use collapsible sidebars, tabs, or pagination to disclose details
116* The chat sheet maintains conversational context
115117 
116**On mobile:** Your app fills the entire screen with the chat input and navigation bar overlaid on top, so keep critical UI within the safe area. Use the full viewport width and support both portrait and landscape where it makes sense.
118On mobile, your app fills the entire screen with the chat input and navigation bar overlaid on top, so keep critical UI within the safe area. Use the full viewport width, and support both portrait and landscape where it makes sense.
117119 
118120<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/fullscreen-mobile.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=d2802346090fd1a7af6a1bdc1d7c3263" alt="Full screen mobile examples" width="1999" height="716" data-path="images/mcp-apps/fullscreen-mobile.png" />
119121 
120122## Mobile guidelines
121123 
122MCP apps on mobile share the same principles as web, but the constrained viewport and touch-based interaction require specific adaptations.
124MCP Apps on mobile follow the same principles as on web, but the constrained viewport and touch-based interaction require specific adaptations. On mobile, Claude renders apps in a native WebView, `WKWebView` on iOS and `WebView` on Android, rather than a sandboxed iframe. Apps on mobile have no camera, microphone, or location access, and users must add a connector on web or desktop before it appears on mobile.
123125 
124On mobile, Claude renders apps in a native WebView (WKWebView on iOS, WebView on Android) rather than a sandboxed iframe. Current mobile-only constraints: no camera/mic/location access, and connectors must be added via web or desktop before they appear on mobile.
125 
126126### Host context for layout
127127 
128The host passes layout hints via `hostContext`.
128The host passes layout hints to your app through `hostContext`. Apps always fill the container width, with no fixed breakpoints, so design responsively from 320px up to fullscreen using container queries and the `hostContext` CSS variables.
129129 
130**Safe areas.** The interactive portion of your app should be rendered inside of the safe area to ensure it's not obscured by the mobile navigation bar or chat input and respects the chat screen's content margins. The user won't be able to interact with anything rendered outside the safe area (e.g. buttons obscured by a mobile navigation bar). Read `hostContext.safeAreaInsets.{top, right, bottom, left}` (in pixels) and apply them as padding on your root container, or as `scroll-padding` on scroll-snap containers so items come to rest inside the visible region. Safe areas are not mobile-specific: on web and desktop the composer can overlay the bottom of an inline app, so avoid placing interactive controls flush against any edge.
130#### Safe areas
131131 
132Render the interactive portion of your app inside the safe area so the mobile navigation bar and chat input don't obscure it and it respects the chat screen's content margins. The user can't interact with anything rendered outside the safe area, such as a button under the mobile navigation bar.
133 
134Read `hostContext.safeAreaInsets.{top, right, bottom, left}`, which are pixel values, and apply them as padding on your root container, or as `scroll-padding` on scroll-snap containers so items come to rest inside the visible region. Safe areas aren't mobile-specific: on web and desktop the composer can overlay the bottom of an inline app, so avoid placing interactive controls flush against any edge.
135 
132136<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/safe-area-fullscreen.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=d789bcbba50a3bff53d677478421de30" alt="Safe area insets in full screen mode on web and mobile" width="1999" height="1153" data-path="images/mcp-apps/safe-area-fullscreen.png" />
133137 
134**Borderless inline.** Set `_meta.ui.prefersBorder` to true or false to explicitly determine whether your content should render with a border. If no value is specified, content will be rendered borderless on web and bordered on mobile. In borderless mode your content runs edge-to-edge with no host padding, so honoring `safeAreaInsets` becomes essential; the bordered card's built-in padding otherwise absorbs most of them. Borderless works well for carousels and other horizontally-scrolling content that should bleed to the screen edges while in motion: apply `safeAreaInsets.left` and `.right` as `scroll-padding-inline` on the scroll container so items at rest sit clear of the device edges, but can scroll underneath them.
138#### Borderless inline content
135139 
136<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/safe-area-borderless-mobile.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=9936d8064beb2fc37e39b10ab1e727a9" alt="Safe area insets for borderless inline apps on mobile" width="1999" height="1857" data-path="images/mcp-apps/safe-area-borderless-mobile.png" />
140Set `_meta.ui.prefersBorder` to `true` or `false` to control whether your content renders with a border. If you don't set it, content renders borderless on web and bordered on mobile.
137141 
138Apps always fill the container width—there are no fixed breakpoints. Design responsively from 320px up to fullscreen using container queries and the hostContext CSS variables.
142In borderless mode your content runs edge-to-edge with no host padding, so honoring `safeAreaInsets` becomes essential. The bordered card's built-in padding otherwise absorbs most of them. Borderless works well for carousels and other horizontally scrolling content that should bleed to the screen edges while in motion: apply `safeAreaInsets.left` and `.right` as `scroll-padding-inline` on the scroll container so items at rest sit clear of the device edges but can scroll underneath them.
139143 
140### Display modes
144<img src="https://mintcdn.com/claude-ai/sLZLADaApRAVEV6C/images/mcp-apps/safe-area-borderless-mobile.png?fit=max&auto=format&n=sLZLADaApRAVEV6C&q=85&s=9936d8064beb2fc37e39b10ab1e727a9" alt="Safe area insets for borderless inline apps on mobile" width="1999" height="1857" data-path="images/mcp-apps/safe-area-borderless-mobile.png" />
141145 
142Declare which modes your app supports via `appCapabilities.availableDisplayModes` in `ui/initialize`. The host responds with the modes it supports, and your app can request a switch with `ui/request-display-mode`. Modes are `inline`, `fullscreen`, and `pip`.
146### Declare supported display modes
143147 
148Declare which modes your app supports through `appCapabilities.availableDisplayModes` in `ui/initialize`. The host responds with the modes it supports, and your app can request a switch with `ui/request-display-mode`. Modes are `inline`, `fullscreen`, and `pip`.
149 
144150### Content security policy
145151 
146Declare external origins per `ui://` resource via `_meta.ui.csp`:
152All external origins are blocked by default. Declare the origins each `ui://` resource needs through `_meta.ui.csp`:
147153 
148154```json theme={null}
149155{
from line 165
159165}
160166```
161167 
162By default, all external origins are blocked. `frameDomains` (embedding third-party iframes) is currently restricted in Claude pending security review.
168The `frameDomains` field, for embedding third-party iframes, is restricted in Claude pending security review.
163169 
164170### Viewport and layout
165171 
166* Design for variable widths (320pt minimum, up to tablet)
172* Design for variable widths, from a 320pt minimum up to tablet
167173* Respect safe areas on notched devices
168* Full-width layouts — don't add side margins that waste mobile screen real estate
169* Content should reflow gracefully; avoid fixed-width layouts
174* Use full-width layouts, without side margins that waste mobile screen space
175* Let content reflow gracefully, and avoid fixed-width layouts
170176 
171177<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/mobile-viewport-layout.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=d2244b6ff7e2d6c5e7b2c36fd2d901ed" alt="Viewport and layout do's and don'ts" width="1718" height="1238" data-path="images/mcp-apps/mobile-viewport-layout.png" />
172178 
173179### Touch targets
174180 
175* Minimum tap target: 44 x 44pt (per Apple HIG / Material guidelines)
176* Add sufficient spacing between interactive elements to prevent mis-taps
177* Prefer larger, thumb-friendly buttons over small text links
178* Place primary actions within natural thumb reach (lower portion of screen)
181* Minimum tap target of 44 x 44pt, per the Apple HIG and Material guidelines
182* Sufficient spacing between interactive elements to prevent mis-taps
183* Larger, thumb-friendly buttons rather than small text links
184* Primary actions within natural thumb reach, in the lower portion of the screen
179185 
180186<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/mobile-touch-targets.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=95bd5b4393d85a67b74e894331d5f4b0" alt="Touch target do's and don'ts" width="1718" height="1238" data-path="images/mcp-apps/mobile-touch-targets.png" />
181187 
182188### Scrolling and gestures
183189 
184On mobile touch devices, the conversation view owns vertical scrolling. When your app is rendered inline, vertical pan gestures that start inside it are passed to the conversation scroll instead of to your content. This keeps a tall widget from trapping the user and is why inline apps should fit their content height rather than relying on an internal vertical scroll container—the host caps inline height and clips content that exceeds it.
190On mobile touch devices, the conversation view owns vertical scrolling. When your app is rendered inline, vertical pan gestures that start inside it go to the conversation scroll instead of to your content, so a tall widget can't trap the user. The host also caps inline height and clips content that exceeds it, so fit your inline app to its content height rather than relying on an internal vertical scroll container.
185191 
186Horizontal gestures (ex: carousels or panning a map) and taps work normally.
192Horizontal gestures, such as swiping a carousel or panning a map, and taps work normally.
187193 
188If your app genuinely needs its own vertically scrollable viewport, request fullscreen presentation with `ui/request-display-mode` instead of rendering inline (see [Full screen](#full-screen)).
194If your app needs its own vertically scrollable viewport, request fullscreen presentation with `ui/request-display-mode` instead of rendering inline, as described in [Full screen](#full-screen).
189195 
190196### Transitions
191197 
192198* Inline cards expand to fullscreen with a smooth transition
193* Provide a clear visual affordance for expansion (fullscreen button or tap-to-expand)
194* Fullscreen close returns to the conversation at the same scroll position
199* Provide a clear visual affordance for expansion, such as a fullscreen button or tap-to-expand
200* Closing fullscreen returns to the conversation at the same scroll position
195201 
196202<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/mobile-transitions.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=343fe75a1428c1874568c10e02de9aa8" alt="Transition from inline card to full screen" width="1718" height="1222" data-path="images/mcp-apps/mobile-transitions.png" />
197203 
198204### Dark mode
199205 
200All views must support both light and dark themes. Use the host's style tokens — they automatically adapt. Never hardcode colors. Test both modes.
206All views must support both light and dark themes. Use the host's style tokens, which adapt automatically, and never hardcode colors. Test both modes.
201207 
202208<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/mobile-dark-mode.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=79df1abf42e75b052ad7c28ac71c4413" alt="Dark mode examples on mobile" width="1999" height="1315" data-path="images/mcp-apps/mobile-dark-mode.png" />
203209 
204210### Loading states
205211 
206Show skeleton screens while content loads. Match the layout structure of the final content so the transition feels seamless. Avoid spinners for inline content — skeletons feel more native.
212Show skeleton screens while content loads, and match the layout structure of the final content so the swap is smooth. Avoid spinners for inline content, because skeletons feel more native.
207213 
208214<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/mobile-loading-states.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=9e7df0fa2294bbcbcc24bf079cbc5ff6" alt="Loading state examples on mobile" width="1718" height="1130" data-path="images/mcp-apps/mobile-loading-states.png" />
209215 
210216## Visual design
211217 
212MCP Apps should feel native to their environment while maintaining consistent visual hierarchy and accessibility standards. You can still express your brand through accent colors, custom controls and content.
218MCP Apps should feel native to their environment while maintaining consistent visual hierarchy and accessibility standards. You can still express your brand through accent colors, custom controls, and content.
213219 
214**Design guidance**
220### Color
215221 
216**Color.** Use host tokens for all structural elements: backgrounds, text, borders, icons. You can use your own brand colors for accents and identity, but the core UI should use the provided palette.
222Use host tokens for all structural elements: backgrounds, text, borders, and icons. You can use your own brand colors for accents and identity, but the core UI should use the provided palette.
217223 
218224<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/color-tokens.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=173ce1948ae87b9e72b0854b5d27f4e1" alt="Color token examples for light and dark mode" width="1999" height="1863" data-path="images/mcp-apps/color-tokens.png" />
219225 
220**Typography.** Stick to the three-level size scale (heading, body, caption) and two weights (regular, emphasized). This creates clear hierarchy without visual noise.
226### Typography
221227 
228Stick to the three-level size scale of heading, body, and caption, and the two weights of regular and emphasized. This creates clear hierarchy without visual noise.
229 
222230<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/typography.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=51ce152aab3d975ae57c5e5792dc6097" alt="Typography scale examples" width="1999" height="936" data-path="images/mcp-apps/typography.png" />
223231 
224232<Note>
225 The Figma Community File uses Anthropic Sans and Anthropic Serif. When your app runs inside Claude, the host client provides Anthropic Sans at runtime through style variables. [Download and install the fonts from here](https://brand.anthropic.com/typography) for local development.
233 The Figma Community File uses Anthropic Sans and Anthropic Serif. When your app runs inside Claude, the host client provides Anthropic Sans at runtime through style variables. For local development, [download and install the fonts](https://brand.anthropic.com/typography).
226234</Note>
227235 
228**Borders.** Using a limited set of corner radii and thickness will keep your app feeling native to the surrounding UI.
236### Borders
229237 
238Use a limited set of corner radii and border thicknesses to keep your app feeling native to the surrounding UI.
239 
230240<img src="https://mintcdn.com/claude-ai/IPtAfld1XUBVOx8m/images/mcp-apps/borders.png?fit=max&auto=format&n=IPtAfld1XUBVOx8m&q=85&s=1755cc995399c9d6bf25a1405ff139f4" alt="Border radius examples" width="1999" height="201" data-path="images/mcp-apps/borders.png" />
231241 
232**Icons.** Use monochromatic, outlined icons that match the host's icon color tokens. Icons should support understanding, not be essential to it.
242### Icons
233243 
244Use monochromatic, outlined icons that match the host's icon color tokens. Icons should support understanding rather than be essential to it.
245 
234246<img src="https://mintcdn.com/claude-ai/IXMfN0TT8kfXbN6T/images/mcp-apps/icons.png?fit=max&auto=format&n=IXMfN0TT8kfXbN6T&q=85&s=1454f1f3e5e13107cbe543e8d6d617df" alt="Icon style examples" width="1320" height="768" data-path="images/mcp-apps/icons.png" />
235247 
236**Spacing.** Maintain generous padding and logical groupings. Balance information density with readability, adapting to viewport constraints.
248### Spacing
237249 
250Maintain generous padding and logical groupings. Balance information density with readability, adapting to viewport constraints.
251 
252### Accessibility
253 
254Your app must be usable by everyone. Maintain high contrast at WCAG AA minimum, support keyboard navigation, and provide text alternatives for visual content. Test with assistive technologies.
255 
238256## Interaction patterns
239257 
240### App vs. chat interactions
258Some interactions belong inside your app and others belong in Claude's chat input. Drawing that boundary correctly, revealing complexity progressively, and keeping controls visible make your app feel cohesive with the conversation.
241259 
242Understanding the boundary between your app's interactions and Claude's conversational interface helps you build something that feels cohesive.
260### Decide between app and chat interactions
243261 
244**Handle within your app:**
262Handle these within your app:
245263 
246264* Direct manipulation like sliders, toggles, and selections
247265* Filtering or sorting data you're already displaying
248266* Expanding and collapsing content sections
249* Confirming or executing a prepared action ("Mark complete," "Send," "Save")
250* Interacting with visualizations like hover states or clicking data points
267* Confirming or executing a prepared action, such as "Mark complete," "Send," or "Save"
268* Interacting with visualizations, like hover states or clicking data points
251269 
252Prefer controls with visible options (segmented buttons, toggle chips, inline tabs) over menus and dropdowns, which can conflict with the host container.
270Push these to the chat input:
253271 
254**Push to chat input:**
255 
256272* Text entry and freeform input
257273* Follow-up questions or requests for clarification
258274* Requests to modify, refine, or redo something
from line 277
261277 
262278If the interaction requires language understanding or generates a response from Claude, it goes through chat. If it's a direct UI action on content your app already controls, handle it in the app.
263279 
264### Start simple
280### Reveal complexity progressively
265281 
266Reveal complexity only when users need it. The inline card might show a summary; fullscreen mode can offer the detailed view.
282Reveal complexity only when users need it. The inline card might show a summary, and fullscreen mode can offer the detailed view.
267283 
268### Visible controls over hidden menus
284### Prefer visible controls over hidden menus
269285 
270Prefer controls with visible options (segmented buttons, toggle chips, inline tabs) over menus and dropdowns. Menus conflict with the host container and are harder to use on mobile.
286Prefer controls with visible options, such as segmented buttons, toggle chips, and inline tabs, over menus and dropdowns. Menus conflict with the host container and are harder to use on mobile.
271287 
272## Accessibility
273 
274Maintain high contrast standards (WCAG AA minimum). Support keyboard navigation and provide text alternatives for visual content. Test with assistive technologies. Your app must be usable by everyone.
275 
276288## Style variables
277289 
278MCP Apps automatically receive style variables from the host client. Reference these CSS custom properties to create interfaces that feel native to Claude.
290MCP Apps automatically receive style variables from the host client. Reference these CSS custom properties to create interfaces that feel native to Claude. [Blend your MCP App with Claude's theme](/docs/connectors/building/mcp-apps/transparent-theming) shows how to apply them at runtime.
279291 
280**Color tokens** cover backgrounds, text, and borders. Semantic accent colors signal status. All tokens automatically adapt to light and dark mode.
292### Color tokens
281293 
294Color tokens cover backgrounds, text, and borders, and semantic accent colors signal status. All tokens automatically adapt to light and dark mode.
295 
282296| | Light mode | Dark mode |
283297| :--------------------------- | :-------------- | :-------------- |
284298| **Background** | | |
from line 337
323337| `color-ring-success` | `#437426 (50%)` | `#599130 (50%)` |
324338| `color-ring-warning` | `#805C1F (50%)` | `#A87829 (50%)` |
325339 
326**Typography tokens** include the font family, sizes, weights and line heights.
340### Typography tokens
327341 
342Typography tokens include the font family, sizes, weights, and line heights.
343 
328344| Family | |
329345| :----------------------------- | :----------------------------- |
330346| `font-sans` | `"Anthropic Sans, sans-serif"` |
from line 375
359375| `font-heading-2xl-line-height` | `1.1` |
360376| `font-heading-3xl-line-height` | `1` |
361377 
362**Radius tokens** provide border radius values
378### Radius tokens
363379 
380Radius tokens provide border radius values.
381 
364382| Radius | |
365383| :------------------- | :------- |
366384| `border-radius-xs` | `4px` |
from line 388
370388| `border-radius-xl` | `12px` |
371389| `border-radius-full` | `9999px` |
372390 
373**Border width tokens** provide width values
391### Border width tokens
374392 
393Border width tokens provide border width values.
394 
375395| | |
376396| :--------------------- | :------ |
377397| `border-width-regular` | `0.5px` |
378398 
379**Shadow tokens** provide drop-shadow values
399### Shadow tokens
380400 
401Shadow tokens provide drop-shadow values.
402 
381403| | |
382404| :---------------- | :----------------------------------------------------------------------- |
383405| `shadow-hairline` | `0 1px 2px 0 rgba(0, 0, 0, 0.05)` |
from line 409
387409 
388410### Example usage
389411 
412This CSS applies the host tokens to an app container, a card, and a button:
413 
390414```css theme={null}
391415.my-app {
392416 background: var(--color-background-primary);
from line 433
409433 border-radius: var(--border-radius-md);
410434}
411435```
436 
437## Related resources
438 
439* [Blend your MCP App with Claude's theme](/docs/connectors/building/mcp-apps/transparent-theming): apply the style variables and keep your background transparent
440* [Set `ui.domain` for Claude](/docs/connectors/building/mcp-apps/getting-started#set-ui-domain-for-claude): compute the sandbox origin Claude expects for your app
441* [Submit a connector](/docs/connectors/building/submission#carousel-screenshots-for-mcp-apps): screenshot specifications for listing an MCP App in the directory
412442 

connectors/building/mcp-apps/external-links Changed · +28 / -22 lines

# Open external links from MCP Apps ## Default link behavior ## Allowlist link destinations ### Allowlist example ## Next steps # Opening external links from MCP Apps ## Default behavior ## Allowlisting link destinations ### Example

from line 1
1# Opening external links from MCP Apps
1# Open external links from MCP Apps
22 
3> How Claude handles ui/open-link requests, and how directory connectors can allowlist destinations to skip the confirmation modal
3> Declare allowed link destinations so ui/open-link requests from your directory connector's MCP App open without Claude's confirmation modal.
44 
5When your MCP App sends a `ui/open-link` request, Claude shows an "Open external link" confirmation modal before navigating. This protects users from being silently redirected by an embedded app.
5When your MCP App sends a `ui/open-link` request, Claude shows an **Open external link** confirmation modal before navigating. The modal protects users from being silently redirected by an embedded app.
66 
7Directory connectors can declare a set of trusted destinations that open immediately without the modal. Custom connectors and locally configured servers always show the modal.
7If your connector is published in the [Connectors Directory](/docs/connectors/directory), you can declare a set of trusted destinations that open immediately without the modal. Custom connectors and locally configured servers always show it. To skip the modal for a directory connector, [allowlist your destinations](#allowlist-link-destinations) and send each request after a [real user gesture](#user-activation-requirement), then [design your app](#design-for-the-modal) for the cases where the modal still appears.
88 
9## Default behavior
9## Default link behavior
1010 
11A `ui/open-link` request displays a confirmation modal showing the destination URL. The link opens in a new tab when the user confirms; the request resolves as cancelled if they dismiss the modal.
11A `ui/open-link` request displays a confirmation modal showing the destination URL. If the user confirms, the link opens in a new tab. If they dismiss the modal, the request resolves as cancelled.
1212 
13## Allowlisting link destinations
13## Allowlist link destinations
1414 
15If your connector is published in the [Connectors Directory](/docs/connectors/directory), you can declare destinations that skip the modal. Provide them in the **Allowed link URIs** field when you [submit](/docs/connectors/building/submission) or update your directory listing.
15A directory connector declares the destinations that skip the modal in its directory listing. Provide them in the **Allowed link URIs** field when you [submit](/docs/connectors/building/submission#allowed-link-uris) or update your listing.
1616 
17Each entry must be one of two shapes:
17Each entry must be an HTTPS origin or a custom URI scheme:
1818 
1919| Entry shape | Example | Matches |
2020| ----------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21| HTTPS origin | `https://docs.example.com` | Any `https://` URL whose hostname is exactly `docs.example.com` (case-insensitive). Subdomains do not match implicitly; list each one you need. Port is not compared. |
21| HTTPS origin | `https://docs.example.com` | Any `https://` URL whose hostname is exactly `docs.example.com`, case-insensitive. Subdomains don't match implicitly, so list each one you need. Port isn't compared. |
2222| Custom URI scheme | `example-app` or `example-app:` | Any URL with the scheme `example-app:`, typically a deep link into your native mobile or desktop app. |
2323 
24Entries that do not fit one of these shapes are ignored. This includes bare hostnames such as `example.com`, `http://` origins, and malformed values.
24Entries that don't fit one of these shapes are ignored. This includes bare hostnames such as `example.com`, `http://` origins, and malformed values.
2525 
26### Example
26### Allowlist example
2727 
2828Given the following allowlist:
2929 
from line 41
4141 
4242These destinations still show the confirmation modal:
4343 
44* `https://blog.example.com` (subdomain not listed)
45* `http://example.com` (not HTTPS)
46* `https://example.com.attacker.net` (different hostname)
44* `https://blog.example.com`, because the subdomain isn't listed
45* `http://example.com`, because it isn't HTTPS
46* `https://example.com.attacker.net`, because the hostname is different
4747 
4848### Restrictions on custom schemes
4949 
from line 51
5151 
5252## User-activation requirement
5353 
54The modal is bypassed only when the `ui/open-link` request follows a real user gesture in your app, such as a button click.
54Claude bypasses the modal only when the `ui/open-link` request follows a real user gesture in your app, such as a button click.
5555 
56If your app sends `ui/open-link` without a preceding gesture (programmatically, on a timer, or after the browser's activation window has expired), the modal is shown so the user's confirmation click supplies the gesture the browser requires to open a new tab.
56If your app sends `ui/open-link` without a preceding gesture, for example programmatically, on a timer, or after the browser's activation window has expired, Claude shows the modal so the user's confirmation click supplies the gesture the browser requires to open a new tab.
5757 
5858<Note>
59 A bypassed `ui/open-link` request resolves successfully once the open is attempted; it does not indicate whether the browser actually opened the tab. Do not treat the response as confirmation that the user reached the destination.
59 A bypassed `ui/open-link` request resolves successfully once the open is attempted. It doesn't indicate whether the browser actually opened the tab, so don't treat the response as confirmation that the user reached the destination.
6060</Note>
6161 
6262## Design for the modal
6363 
64Even with an allowlist configured, your app should remain usable when the modal appears:
64Even with an allowlist configured, the modal still appears in some cases, so your app should remain usable when it does:
6565 
66* Custom and local connectors always show the modal. Your app may run outside the directory during development or in self-hosted deployments.
67* Destinations not on your allowlist, or added since your last published directory update, show the modal.
68* Requests without user activation show the modal.
66* Custom and local connectors always show the modal, and your app may run outside the directory during development or in self-hosted deployments
67* Destinations not on your allowlist, or added since your last published directory update, show the modal
68* Requests without user activation show the modal
6969 
7070Provide enough context in your UI that the destination URL shown in the modal is recognizable to the user.
71 
72## Next steps
73 
74* [Submit a connector](/docs/connectors/building/submission#allowed-link-uris): where the **Allowed link URIs** field fits in your directory submission
75* [Design guidelines](/docs/connectors/building/mcp-apps/design-guidelines#interaction-patterns): which interactions belong in your app and which belong in chat
76* [Troubleshoot MCP Apps](/docs/connectors/building/mcp-apps/troubleshooting): developer tools for inspecting your app's requests
7177 

connectors/building/mcp-apps/getting-started Changed · +122 / -80 lines

## Try an example MCP App in Claude Desktop ### Ask Claude to use the app ### Register tools and resources once for every host ### Set `ui.domain` for Claude ### Build with an AI coding agent ### Migrate from the OpenAI Apps SDK ## Send feedback on MCP Apps ## Next steps ## Try an example MCP App ### See it in action ## Migrate from OpenAI Apps SDK

from line 1
11# Get started with MCP Apps
22 
3> Learn how to test MCP Apps in Claude
3> Try example MCP Apps in Claude Desktop, then use the MCP Apps SDK, examples, and agent skills to add interactive UI to your own MCP server.
44 
5## Try an example MCP App
5An MCP App is interactive UI that your MCP server renders inside a Claude conversation, such as an interactive chart or map. You build one with the [MCP Apps SDK](https://modelcontextprotocol.github.io/ext-apps/api/index.html) as part of your own MCP server, so it reaches people the same way the rest of your plugin's connector does.
66 
7This page is for developers who have an MCP server, or are building one, and want it to show UI in Claude.
8 
9<Note>
10 If you haven't built the server yet, see [Build an MCP server for Claude](/docs/connectors/building/index).
11</Note>
12 
13To see how an MCP App looks and behaves, [try an example in Claude Desktop](#try-an-example-mcp-app-in-claude-desktop) first, then [build your own](#build-your-own-mcp-app) from the SDK quickstart, the examples, or the agent skills.
14 
15## Try an example MCP App in Claude Desktop
16 
17The MCP Apps repository publishes example servers that you run locally with `npx` and connect to Claude Desktop through its configuration file.
18 
719### Connect an example server
820 
9Make sure you have installed and logged into Claude Desktop. Navigate to the [developer settings page](https://claude.ai/desktop/settings/desktop/developer) (**Settings > Developer**) and click the "Edit Config" button.
21Connecting an example server means adding its entry to Claude Desktop's configuration file, so that the desktop app runs the server locally with `npx`. To connect one:
1022 
11Add one of the example servers to your `claude_desktop_config.json`:
23<Steps>
24 <Step title="Install Claude Desktop">
25 Install Claude Desktop and sign in.
26 </Step>
1227 
13| Example | Description |
14| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
15| [**Customer Segmentation**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/customer-segmentation-server) | Data visualization with scatter charts and clustering analysis |
16| [**Map**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) | Interactive 3D globe viewer using CesiumJS |
17| [**QR Code**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) | QR code generation with customizable colors and styling |
18| [**ShaderToy**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) | Real-time GLSL shader compilation and display |
19| [**Sheet Music**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) | ABC notation rendering with interactive audio playback |
20| ⋮ | Explore [more examples](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples)—each with ready-to-use config snippets! |
28 <Step title="Open the configuration file">
29 Go to [**Settings > Developer**](https://claude.ai/desktop/settings/desktop/developer) and click **Edit Config**.
30 </Step>
2131 
22<CodeGroup>
23 ```json Customer Segmentation theme={null}
24 {
25 "mcpServers": {
26 "customer-segmentation": {
27 "command": "npx",
28 "args": ["-y", "@modelcontextprotocol/customer-segmentation-server", "--stdio"]
29 }
30 }
31 }
32 ```
32 <Step title="Add an example server">
33 Add one of these example servers to your `claude_desktop_config.json`:
3334 
34 ```json Map theme={null}
35 {
36 "mcpServers": {
37 "map": {
38 "command": "npx",
39 "args": ["-y", "@modelcontextprotocol/map-server", "--stdio"]
35 | Example | Description |
36 | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
37 | [Customer Segmentation](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/customer-segmentation-server) | Data visualization with scatter charts and clustering analysis |
38 | [Map](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) | Interactive 3D globe viewer using CesiumJS |
39 | [ShaderToy](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) | Real-time GLSL shader compilation and display |
40 | [Sheet Music](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) | ABC notation rendering with interactive audio playback |
41 
42 The MCP Apps repository has [more examples](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples), each with a ready-to-use config snippet. The config entry for each example in the table runs its server with `npx`, which fetches the latest published version each time. Pin a version, such as `@modelcontextprotocol/[email protected]`, if you keep an entry beyond trying it out:
43 
44 <CodeGroup>
45 ```json Customer Segmentation theme={null}
46 {
47 "mcpServers": {
48 "customer-segmentation": {
49 "command": "npx",
50 "args": ["-y", "@modelcontextprotocol/server-customer-segmentation", "--stdio"]
51 }
52 }
4053 }
41 }
42 }
43 ```
54 ```
4455 
45 ```json QR Code theme={null}
46 {
47 "mcpServers": {
48 "qr": {
49 "command": "npx",
50 "args": ["-y", "@modelcontextprotocol/qr-server", "--stdio"]
56 ```json Map theme={null}
57 {
58 "mcpServers": {
59 "map": {
60 "command": "npx",
61 "args": ["-y", "@modelcontextprotocol/server-map", "--stdio"]
62 }
63 }
5164 }
52 }
53 }
54 ```
65 ```
5566 
56 ```json ShaderToy theme={null}
57 {
58 "mcpServers": {
59 "shadertoy": {
60 "command": "npx",
61 "args": ["-y", "@modelcontextprotocol/shadertoy-server", "--stdio"]
67 ```json ShaderToy theme={null}
68 {
69 "mcpServers": {
70 "shadertoy": {
71 "command": "npx",
72 "args": ["-y", "@modelcontextprotocol/server-shadertoy", "--stdio"]
73 }
74 }
6275 }
63 }
64 }
65 ```
76 ```
6677 
67 ```json Sheet Music theme={null}
68 {
69 "mcpServers": {
70 "sheet-music": {
71 "command": "npx",
72 "args": ["-y", "@modelcontextprotocol/sheet-music-server", "--stdio"]
78 ```json Sheet Music theme={null}
79 {
80 "mcpServers": {
81 "sheet-music": {
82 "command": "npx",
83 "args": ["-y", "@modelcontextprotocol/server-sheet-music", "--stdio"]
84 }
85 }
7386 }
74 }
75 }
76 ```
77</CodeGroup>
87 ```
88 </CodeGroup>
89 </Step>
7890 
79Save and restart the desktop app to connect.
91 <Step title="Save and restart">
92 Save the file and restart the desktop app to connect the server.
93 </Step>
94</Steps>
8095 
81### See it in action
96### Ask Claude to use the app
8297 
83Once your local server is connected, prompt Claude to use it. For example, with the customer segmentation server, ask Claude to show you recent customer data.
98Once the server is connected, prompt Claude to use it. For example, with the customer segmentation server connected, ask Claude to show you recent customer data.
8499 
85Claude will prompt you for permission to display the App. Click "Always allow", and you'll see the MCP App render inline in the conversation.
100Claude asks for permission to display the app. Click **Allow**, or **Always allow** for a server you trust, and the MCP App renders inline in the conversation.
86101 
87102## Build your own MCP App
88103 
89Ready to add an MCP App to your own MCP server? Here are the key resources:
104To add an MCP App to your own MCP server, start from the SDK documentation and examples:
90105 
91* [MCP Apps Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) - Step-by-step guide to building your first MCP App
92* [SDK API Documentation](https://modelcontextprotocol.github.io/ext-apps/api/index.html) - Full API reference
93* [Example implementations](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) - Vanilla JS, React, Vue, Svelte, and more
106* [Add an interactive UI to your MCP server](/docs/connectors/building/mcp-apps/quickstart): add a one-tool MCP App to the quickstart server on this site, in plain JavaScript with no build step
107* [MCP Apps Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html): step-by-step guide to building your first MCP App
108* [SDK API documentation](https://modelcontextprotocol.github.io/ext-apps/api/index.html): full API reference
109* [Example implementations](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples): vanilla JS, React, Vue, Svelte, and more
94110 
95If you are using an AI coding agent, [MCP Apps skills](https://github.com/modelcontextprotocol/ext-apps/tree/main/plugins/mcp-apps) provide guided development for agents that support the [Agent Skills](https://agentskills.io) standard, including Claude Code, Cursor, Gemini CLI, and others. In Claude Code, you can install the MCP Apps skills plugin with the following commands:
111<Tip>To test a remote MCP App locally, connect to it through a proxy such as [mcp-remote](https://www.npmjs.com/package/mcp-remote).</Tip>
96112 
113### Register tools and resources once for every host
114 
115An MCP App can run in Claude and in other hosts that support MCP Apps from one codebase. On the server, register your tools and resources with [`registerAppTool()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/server-helpers.registerAppTool.html) and [`registerAppResource()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/server-helpers.registerAppResource.html), which generate each host's metadata for you. In the app, call [`App.connect()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#connect) without a transport argument, and the SDK detects the host and picks the transport.
116 
117### Set `ui.domain` for Claude
118 
119Each host defines its own format for the [`Resource._meta.ui.domain`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#domain) field, the sandbox origin your app is served from. For Claude, the value is the first 32 hex characters of the SHA-256 of your server URL, followed by `.claudemcpcontent.com`. Compute it with this command, replacing `https://example.com/mcp` with your server URL:
120 
121```shell theme={null}
122node -e 'const yourServerUrl = "https://example.com/mcp"; console.log(require("crypto").createHash("sha256").update(yourServerUrl).digest("hex").slice(0,32) + ".claudemcpcontent.com")'
97123```
124 
125For `https://example.com/mcp`, the command prints:
126 
127```text theme={null}
128c3d80a4ed901ee05b21755a88273b4a4.claudemcpcontent.com
129```
130 
131If Claude reports `Invalid ui.domain format` or `ui.domain mismatch`, see [Troubleshoot MCP Apps](/docs/connectors/building/mcp-apps/troubleshooting#ui-domain-validation-fails).
132 
133### Build with an AI coding agent
134 
135If you use an AI coding agent, the [MCP Apps skills](https://github.com/modelcontextprotocol/ext-apps/tree/main/plugins/mcp-apps) guide it through MCP App development. They work in any agent that supports the [Agent Skills](https://agentskills.io) standard, including Claude Code. In Claude Code, install the MCP Apps skills plugin with these commands:
136 
137```text theme={null}
98138/plugin marketplace add modelcontextprotocol/ext-apps
99/plugin install mcp-apps@modelcontextprotocol-ext-apps
139/plugin install mcp-apps@mcp-apps
100140```
101141 
102Once installed, ask your agent to "Create an MCP App" or "Add a UI to my MCP tool".
142Once the MCP Apps skills plugin is installed, ask your agent to "Create an MCP App" or "Add a UI to my MCP tool".
103143 
104<Tip>You can test remote MCP Apps locally via a proxy like [mcp-remote](https://www.npmjs.com/package/mcp-remote).</Tip>
144### Migrate from the OpenAI Apps SDK
105145 
106## Migrate from OpenAI Apps SDK
146If you have an existing app built on the OpenAI Apps SDK, follow the [migration reference](https://modelcontextprotocol.github.io/ext-apps/api/documents/Migrate_OpenAI_App.html) to move it to the MCP Apps SDK. The MCP Apps skills can also do the migration: ask your agent to "Migrate from OpenAI Apps SDK" or "Convert my OpenAI App to an MCP App".
107147 
108If you are migrating an existing app from the OpenAI Apps SDK to the MCP Apps SDK, see the [migration reference](https://modelcontextprotocol.github.io/ext-apps/api/documents/Migrate_OpenAI_App.html).
148## Send feedback on MCP Apps
109149 
110You can also use the MCP Apps skills mentioned above to help migrate your apps. Ask your agent to "Migrate from OpenAI Apps SDK" or "Convert my OpenAI App to an MCP App".
150To report a problem or suggest a change to MCP Apps, email [[email protected]](mailto:[email protected]) or open an issue on the [ext-apps repository](https://github.com/modelcontextprotocol/ext-apps/issues).
111151 
112***
152## Next steps
113153 
114We'd love to see what you build! Send feedback to [[email protected]](mailto:[email protected]) or open an issue on the [ext-apps repository](https://github.com/modelcontextprotocol/ext-apps/issues).
154* [Design guidelines](/docs/connectors/building/mcp-apps/design-guidelines): display modes, mobile layout, and style variables for an app that feels native to Claude
155* [Troubleshoot MCP Apps](/docs/connectors/building/mcp-apps/troubleshooting): developer tools and fixes for an app that doesn't render
156* [Submit a connector](/docs/connectors/building/submission): list your server and its MCP App screenshots in the directory
115157 

connectors/building/mcp-apps/instance-supersession Changed · +49 / -46 lines

## Understand how supersession works ### Understand why the key comes from the server ## Handle production edge cases ### Don't compare server and client timestamps ### Cache the key across remounts ## Next steps ## How it works ### Why not use client-side `Date.now()`? ## Special considerations ### Fallback caveat: don't compare server and client timestamps ### Caching the key across remounts ## Related topics

from line 1
11# Supersede older widget instances
22 
3> Keep only the newest copy of a widget active when its tool is called more than once in a conversation
3> Keep only the newest copy of an MCP App widget active when Claude calls its tool more than once in a conversation, using a server key and BroadcastChannel.
44 
5Each time Claude calls a tool that renders an MCP App, a separate iframe is mounted in the conversation. There is no host API to unmount earlier instances when a newer one appears, so by default you end up with several live copies of the same widget, each independently pushing [model-context updates](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#updatemodelcontext) (data the widget feeds into Claude's context for the next turn) and [messages](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage) to Claude.
5Each time Claude calls a tool that renders an MCP App, Claude mounts a separate iframe in the conversation. No host API unmounts earlier instances when a newer one appears, so by default several live copies of the same widget stay in the conversation. Each copy independently pushes [model-context updates](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#updatemodelcontext), the data a widget feeds into Claude's context for the next turn, and [messages](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage) to Claude.
66 
7If your widget represents a single piece of state, such as a shopping cart or a dashboard, only the most recent instance should remain interactive. You can use [`BroadcastChannel`](https://developer.mozilla.org/docs/Web/API/BroadcastChannel) to make earlier instances disable themselves.
7This page is for MCP App developers whose widget represents a single piece of state, such as a shopping cart or a dashboard, where only the most recent instance should remain interactive. It shows how to use [`BroadcastChannel`](https://developer.mozilla.org/docs/Web/API/BroadcastChannel) so earlier instances disable themselves: [mint an election key on the server](#mint-the-election-key-on-the-server), [run the election in the widget](#run-the-election-in-the-widget), then [handle the production edge cases](#handle-production-edge-cases).
88 
9The snippets on this page assume you have registered a UI resource and tool and created an `App` instance from `@modelcontextprotocol/ext-apps`. See the [SDK Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) if you haven't.
9The snippets assume you have registered a UI resource and tool and created an `App` instance from `@modelcontextprotocol/ext-apps`. If you haven't, start with the [SDK Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html).
1010 
11## How it works
11## Understand how supersession works
1212 
13All widget iframes from a single connector are served from the same sandbox origin on `*.claudemcpcontent.com` (the iframe sandbox includes [`allow-same-origin`](https://developer.mozilla.org/docs/Web/HTML/Element/iframe#sandbox)). That means a `BroadcastChannel` opened in one instance reaches every other instance from the same connector in the current conversation. See [Channel scope and `ui.domain`](#channel-scope-and-ui-domain) for how a fixed domain widens this.
13Claude serves all widget iframes from a single connector from the same sandbox origin on `*.claudemcpcontent.com`, and the iframe sandbox includes [`allow-same-origin`](https://developer.mozilla.org/docs/Web/HTML/Element/iframe#sandbox). A `BroadcastChannel` opened in one instance therefore reaches every other instance from the same connector in the current conversation. A fixed `ui.domain` widens that scope, as [Channel scope and `ui.domain`](#channel-scope-and-ui-domain) explains.
1414 
15The pattern has three parts:
15The supersession pattern uses that shared `BroadcastChannel` to elect the newest instance:
1616 
171. **The server stamps each tool result with an election key.** It returns a `{createdAt, seq}` pair (server wall-clock time and a monotonic counter) in [`structuredContent`](https://modelcontextprotocol.io/specification/latest/server/tools#structured-content), the typed JSON payload slot of an MCP tool result. Tool results are stored in the conversation transcript, so every device and every remount of the widget sees the same key.
182. **Each widget announces its key on a shared channel.** Shortly after `connect()` resolves, the host delivers the tool result that mounted this widget (including its `structuredContent`) via the SDK's [`toolresult` event](https://modelcontextprotocol.github.io/ext-apps/api/types/app.AppEventMap.html). The widget reads its key from that event, opens a `BroadcastChannel`, and broadcasts the key.
193. **Any widget that sees a younger sibling marks itself superseded.** It greys out its UI, disables its buttons, and short-circuits all calls that mutate model context or inject messages.
171. **The server stamps each tool result with an election key**: it returns a `{createdAt, seq}` pair in [`structuredContent`](https://modelcontextprotocol.io/specification/latest/server/tools#structured-content), the typed JSON payload slot of an MCP tool result. `createdAt` is server wall-clock time and `seq` is a monotonic counter. Tool results are stored in the conversation transcript, so every device and every remount of the widget sees the same key.
182. **Each widget announces its key on a shared channel**: shortly after `connect()` resolves, the host delivers the tool result that mounted this widget, including its `structuredContent`, through the SDK's [`toolresult` event](https://modelcontextprotocol.github.io/ext-apps/api/types/app.AppEventMap.html). The widget reads its key from that event, opens a `BroadcastChannel`, and broadcasts the key.
193. **Any widget that sees a younger sibling marks itself superseded**: it greys out its UI, disables its buttons, and short-circuits all calls that mutate model context or inject messages.
2020 
2121## Mint the election key on the server
2222 
23Use [`registerAppTool`](https://modelcontextprotocol.github.io/ext-apps/api/functions/server-helpers.registerAppTool.html) to register the tool, and return the key in `structuredContent` alongside your normal tool output. A per-process counter works for a demo; a production server should derive the key from something durable, such as a database row ID or a version number on the underlying record.
23Use [`registerAppTool`](https://modelcontextprotocol.github.io/ext-apps/api/functions/server-helpers.registerAppTool.html) to register the tool, and return the key in `structuredContent` alongside your normal tool output. A per-process counter works for a demo. A production server should derive the key from something durable, such as a database row ID or a version number on the underlying record. This example registers a `show_cart` tool that returns the key with the cart contents:
2424 
2525```ts theme={null}
2626import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
from line 51
5151);
5252```
5353 
54### Why not use client-side `Date.now()`?
54### Understand why the key comes from the server
5555 
56Client mount time does not reflect tool-call order. When a stored conversation is reopened, Claude lazy-mounts widget cells as they scroll into view, so an older widget can mount after a newer one and would win an election based on client timestamps. The server-minted key is written into the transcript at tool-call time and is identical everywhere.
56Client mount time doesn't reflect tool-call order. When a user reopens a stored conversation, Claude lazy-mounts widget cells as they scroll into view, so an older widget can mount after a newer one and would rank as newest in an election based on client timestamps. The server-minted key is written into the transcript at tool-call time and is identical everywhere.
5757 
5858## Run the election in the widget
5959 
60The four snippets in this section form a single module; paste them in order into your widget entry file.
60The snippets in this section form a single module. Paste them in order into your widget entry file.
6161 
6262### Read the key from the `toolresult` event
6363 
64Connect and read the values you need from the host: your instance ID from [`hostContext.toolInfo`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiHostContext.html#toolinfo), and the server-minted key from the `toolresult` event. The event's `structuredContent` is typed `Record<string, unknown>`, so cast it to the shape your server returns.
64Connect and read the values you need from the host: your instance ID from [`hostContext.toolInfo`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiHostContext.html#toolinfo), and the server-minted key from the `toolresult` event. The event's `structuredContent` is typed `Record<string, unknown>`, so cast it to the shape your server returns:
6565 
6666```ts theme={null}
6767import { App } from "@modelcontextprotocol/ext-apps";
from line 94
9494 
9595### Broadcast and compare on a shared channel
9696 
97Broadcast the key and compare against every sibling you hear from. The comparison is `createdAt`, tie-broken by `seq`, then by instance ID for determinism. Ignore inbound messages until your own key is finalized so you never reply with an undefined key.
97Broadcast the key and compare against every sibling you hear from. The comparison is `createdAt`, tie-broken by `seq`, then by instance ID for determinism. Ignore inbound messages until your own key is finalized so you never reply with an undefined key:
9898 
9999```ts theme={null}
100100const channel = new BroadcastChannel("my-app-cart-supersede");
from line 164
164164}
165165```
166166 
167## Special considerations
167## Handle production edge cases
168168 
169The election above covers the common case. A production widget should also handle the following.
169The election in [Run the election in the widget](#run-the-election-in-the-widget) covers the common case. A production widget also accounts for channel scope under a fixed `ui.domain`, a server key that arrives late, caching the key across remounts, and the requests a custom `postMessage` bridge would drop.
170170 
171171### Channel scope and `ui.domain`
172172 
173`BroadcastChannel` is same-origin only. How far that origin extends depends on whether you set [`_meta.ui.domain`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#domain) on your resource:
173`BroadcastChannel` is same-origin only, and whether you set [`_meta.ui.domain`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#domain) on your resource decides how far that origin extends:
174174 
175* **Without `ui.domain`** (the default), Claude derives the iframe origin from the conversation and connector, so the broadcast is scoped to a single conversation.
176* **With a fixed `ui.domain`**, the origin is shared across every conversation and tab for your connector. A fixed channel name would let a widget in one conversation supersede a widget in another. Neither `hostContext` nor the tool-call arguments include a Claude-provided conversation ID, so if you need both a fixed domain and per-conversation elections, generate your own scope key on the server (for example, a UUID minted once per client connection) and return it in `structuredContent` for the widget to append to the channel name.
175* **Without `ui.domain`, the default**: Claude derives the iframe origin from the conversation and connector, so the broadcast is scoped to a single conversation
176* **With a fixed `ui.domain`**: the origin is shared across every conversation and tab for your connector, so a fixed channel name would let a widget in one conversation supersede a widget in another
177177 
178Neither `hostContext` nor the tool-call arguments include a Claude-provided conversation ID. If you need both a fixed domain and per-conversation elections, generate your own scope key on the server, such as a UUID minted once per client connection, and return it in `structuredContent` for the widget to append to the channel name.
179 
178180### Fall back if the server key is delayed
179181 
180The main snippet above waits for the `toolresult` event before announcing. If you want the widget to participate in the election even when that event is slow to arrive, replace that listener with one that resolves a promise, and race the promise against a short timeout after `connect()`:
182The widget's `toolresult` listener in [Run the election in the widget](#run-the-election-in-the-widget) waits for the event before announcing. If you want the widget to participate in the election even when that event is slow to arrive, replace that listener with one that resolves a promise, and race the promise against a short timeout after `connect()`:
181183 
182184```ts theme={null}
183185let resolveServerKey!: (k: { orderKey: number; seq?: number }) => void;
from line 210
208210 
209211If the server key arrives after the timeout, adopt it, recompute `superseded` against the peers you have already heard from, and re-announce so siblings update their view of you. The recomputed result may flip the instance back to live.
210212 
211### Fallback caveat: don't compare server and client timestamps
213### Don't compare server and client timestamps
212214 
213This applies only if you implemented the fallback above. If you fall back to a client-side `Date.now()` while waiting for the server key, tag the key with its source and refuse to compare a client value against a server value. A server `createdAt` from a tool call made hours ago will always be smaller than a fresh client timestamp, which would wrongly hand "live" to whichever instance happened to fall back. Include `keySource` in the broadcast payload (`announce()` and the `born` reply) and in the `peers` Map value type so siblings can read it:
215If you implemented the delayed-key fallback and fall back to a client-side `Date.now()` while waiting for the server key, tag the key with its source and refuse to compare a client value against a server value. A server `createdAt` from a tool call made hours ago is always smaller than a fresh client timestamp, which would wrongly mark whichever instance happened to fall back as live. Include `keySource` in the broadcast payload, in both `announce()` and the `born` reply, and in the `peers` Map value type so siblings can read it:
214216 
215217```ts theme={null}
216218type KeySource = "server" | "client";
from line 227
225227}
226228```
227229 
228### Caching the key across remounts
230### Cache the key across remounts
229231 
230On Claude.ai web, [`hostContext.toolInfo.id`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiHostContext.html#toolinfo) is the stable tool-use ID, so you can persist the resolved server key to `localStorage` keyed by that ID and reuse it on the next mount without waiting for the `toolresult` event again.
232On claude.ai on the web, [`hostContext.toolInfo.id`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiHostContext.html#toolinfo) is the stable tool-use ID, so you can persist the resolved server key to `localStorage` keyed by that ID and reuse it on the next mount without waiting for the `toolresult` event again.
231233 
232Treat this as an optimization rather than a correctness guarantee. On Claude iOS, `toolInfo.id` is `undefined` when a stored conversation is rehydrated, so there is no stable per-instance cache key. Detect that case and skip the cache; the server key from the `toolresult` event is the only ordering source that works on every platform.
234Treat the `localStorage` cache as an optimization rather than a correctness guarantee. On Claude iOS, `toolInfo.id` is `undefined` when a stored conversation is rehydrated, so there is no stable per-instance cache key. Detect that case and skip the cache; the server key from the `toolresult` event is the only ordering source that works on every platform.
233235 
234236### If you bypass the SDK `App` class
235237 
236The snippets on this page use the SDK's [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class. If you instead hand-roll a minimal `postMessage` bridge, it will silently drop requests sent from the host to the widget, such as `ping` (a liveness check) and [`ui/resource-teardown`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceTeardownRequest.html) (the host asking the widget to clean up before unmount). Claude.ai web does not currently send either to widgets, and Claude iOS sends `ui/resource-teardown` only when the user navigates away from the conversation, so ignoring them is harmless today. The `App` class handles the full request surface and is recommended for production.
238The snippets on this page use the SDK's [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class, which handles the full host request surface and is recommended for production. A minimal `postMessage` bridge you write yourself silently drops requests the host sends to the widget, such as `ping`, a liveness check, and [`ui/resource-teardown`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceTeardownRequest.html), the host's request that the widget clean up before unmount. claude.ai on the web doesn't send either to widgets, and Claude iOS sends `ui/resource-teardown` only when the user navigates away from the conversation, so a bridge that ignores them loses nothing on those hosts.
237239 
238## Related topics
240## Next steps
239241 
240* [Cross-platform compatibility](/docs/connectors/building/mcp-apps/cross-compatibility#domain-handling) for how `_meta.ui.domain` is computed on Claude.
241* [SDK API reference](https://modelcontextprotocol.github.io/ext-apps/api/index.html) for `registerAppTool`, `App`, and `McpUiResourceMeta`.
242* [Set `ui.domain` for Claude](/docs/connectors/building/mcp-apps/getting-started#set-ui-domain-for-claude): how to compute `_meta.ui.domain` for Claude
243* [Troubleshoot MCP Apps](/docs/connectors/building/mcp-apps/troubleshooting): developer tools and fixes when a widget doesn't render
244* [SDK API reference](https://modelcontextprotocol.github.io/ext-apps/api/index.html): `registerAppTool`, `App`, and `McpUiResourceMeta`
242245 

connectors/building/mcp-apps/quickstart New page · 326 lines, new page

# Add an interactive UI to your MCP server ## Install the MCP Apps SDK ## Add the UI to the tool ### The complete file ## Check that the server advertises the UI ## See the UI render in Claude ## Next steps

A whole new page. There's nothing to diff it against, so here is what it says.

# Add an interactive UI to your MCP server

> Add an MCP App to the quickstart MCP server: register a ui:// resource with the MCP Apps SDK, link it to a tool, and check that the server advertises the UI.

An [MCP App](/docs/connectors/building/mcp-apps/getting-started) is interactive UI that your MCP server renders inside a Claude conversation, such as an interactive chart or map. In this quickstart you give the `roll_dice` tool from [Build your first MCP server for Claude](/docs/connectors/building/quickstart) a small UI that draws each die and has a **Roll again** button, by adding about 50 lines to the same `server.mjs` and no build tooling. At the end your server advertises the UI the way an MCP Apps host expects, and you've checked that from the command line.

This quickstart is for developers who finished the first quickstart and have its server on their machine. It stops at what you can verify locally: seeing the UI render needs the server hosted where Claude can reach it, which the last section covers.

<Note>
  * If you haven't built the quickstart server yet, start with [Build your first MCP server for Claude](/docs/connectors/building/quickstart)
  * If you want to see finished MCP Apps running in Claude first, see [Try an example MCP App in Claude Desktop](/docs/connectors/building/mcp-apps/getting-started#try-an-example-mcp-app-in-claude-desktop)
  * If you're adding UI to your own production server, see [Build your own MCP App](/docs/connectors/building/mcp-apps/getting-started#build-your-own-mcp-app) for the SDK documentation and full examples
</Note>

## Install the MCP Apps SDK

The [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps) has two halves: server helpers that attach UI metadata to your tools and resources, and a small browser client that the UI itself loads to talk to the host. One package provides both.

In your terminal, from the `mcp-quickstart` folder, install the `1.x` line of the package, which is the one that pairs with the `1.x` MCP SDK you already have:

```bash theme={null}
npm install @modelcontextprotocol/ext-apps@^1
```

Run `npm ls --depth=0` to confirm. The list now has three packages:

```text theme={null}
[email protected] /path/to/mcp-quickstart
+-- @modelcontextprotocol/[email protected]
+-- @modelcontextprotocol/[email protected]
`-- [email protected]
```

This page was verified with `@modelcontextprotocol/[email protected]`.

## Add the UI to the tool

On the server, an MCP App is a resource with a `ui://` URI whose content is the HTML to render, plus a `_meta.ui.resourceUri` field on the tool that points at that resource. When a host that supports MCP Apps calls the tool, it reads that field, fetches the resource, and renders the HTML in a sandboxed frame.

Make these edits to `server.mjs` in order. If you'd rather paste the whole file, it's in [The complete file](#the-complete-file) at the end of this section.

<Steps>
  <Step title="Import the server helpers">
    Add one import under the existing SDK imports at the top of `server.mjs`. `registerAppTool` and `registerAppResource` wrap the SDK's own `registerTool` and `registerResource` and fill in the UI metadata for you:

    ```js server.mjs {4} theme={null}
    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
    import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
    import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
    import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
    import { z } from 'zod';
    ```
  </Step>

  <Step title="Write the UI as an HTML string">
    Below the imports and above `function buildServer()`, add the resource URI and the HTML the host will render. The HTML is ordinary markup plus one `<script type="module">`. The highlighted lines are the ones that make it an MCP App: the script loads the SDK's browser client, `App`, from a CDN copy of the version you installed, registers handlers for the tool's input and result, wires the button to call the tool again through the host, and then connects:

    ```js server.mjs {1,16,18,26-29} theme={null}
    const DICE_UI = 'ui://quickstart/dice'; // MCP App resources use the ui:// scheme

    const DICE_HTML = `<!DOCTYPE html>
    <html><head><meta charset="utf-8"><meta name="color-scheme" content="light dark"><title>Dice</title>
    <style>
      body { font: 14px system-ui, sans-serif; margin: 0; padding: 12px; }
      #dice { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
      .die { width: 44px; height: 44px; border: 2px solid; border-radius: 10px; display: grid; place-items: center; font-size: 18px; font-weight: 600; }
    </style></head>
    <body>
    <div id="summary">Waiting for a roll…</div>
    <div id="dice"></div>
    <button id="again" disabled>Roll again</button>
    <script type="module">
      // The in-frame client. Loading the prebuilt bundle from a CDN means no bundler in this project.
      import { App } from "https://unpkg.com/@modelcontextprotocol/[email protected]/dist/src/app-with-deps.js";
      const $ = (id) => document.getElementById(id);
      const app = new App({ name: "Dice View", version: "1.0.0" });
      let lastArgs = { sides: 6, count: 1 };
      const show = ({ structuredContent: sc, content }) => {
        $("summary").textContent = sc ? sc.count + "d" + sc.sides + " → total " + sc.total : (content?.[0]?.text ?? "?");
        $("dice").replaceChildren(...(sc?.rolls ?? []).map((n) => Object.assign(document.createElement("div"), { className: "die", textContent: n })));
        $("again").disabled = false;
      };
      // Set handlers before connect() so the first tool-input and tool-result notifications aren't missed.
      app.ontoolinput = ({ arguments: args }) => { if (args) lastArgs = { ...lastArgs, ...args }; };
      app.ontoolresult = show;                     // the host pushes the tool result here; draw it
      $("again").onclick = async () => show(await app.callServerTool({ name: "roll_dice", arguments: lastArgs }));
      await app.connect();                         // handshake with the host
    </script>
    </body></html>`;
    ```

    If you change the installed `ext-apps` version, change the version in the `unpkg.com` URL to match.
  </Step>

  <Step title="Link the tool to the UI">
    Inside `buildServer()`, replace the `server.registerTool(` call with `registerAppTool(server,` and make two additions, both highlighted: a `_meta.ui.resourceUri` entry that names the UI resource, and a `structuredContent` object in the result. The UI reads `structuredContent` to draw the dice, and the `content` text stays for hosts that don't render UI:

    ```js server.mjs {1-2,11,17-18} theme={null}
      registerAppTool(
        server,
        'roll_dice',
        {
          title: 'Roll dice',
          description: 'Roll `count` dice with `sides` sides each. Returns each roll and the total.',
          inputSchema: {
            sides: z.number().int().min(2).max(100).describe('Number of sides per die (2-100)'),
            count: z.number().int().min(1).max(10).default(1).describe('How many dice to roll (1-10)'),
          },
          _meta: { ui: { resourceUri: DICE_UI } }, // tells the host which resource renders this tool
        },
        async ({ sides, count }) => {
          const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
          const total = rolls.reduce((a, b) => a + b, 0);
          return {
            content: [{ type: 'text', text: `Rolled ${count}d${sides}: [${rolls.join(', ')}] total=${total}` }], // text-only hosts read this
            structuredContent: { sides, count, rolls, total }, // the UI reads this
          };
        },
      );
    ```
  </Step>

  <Step title="Register the UI resource">
    Still inside `buildServer()`, before `return server;`, register the resource that serves the HTML. `registerAppResource` sets the MIME type an MCP App resource must have, `text/html;profile=mcp-app`, which the SDK exports as `RESOURCE_MIME_TYPE`. The highlighted `csp` line tells the host's sandbox to allow scripts from `unpkg.com`, because by default the frame only runs scripts that are inline or from its own origin, and the `App` import would be blocked. For production, serve the client script from your own origin, or bundle it into the HTML, and list only that origin in `resourceDomains`:

    ```js server.mjs {4,6} theme={null}
      registerAppResource(server, 'Dice view', DICE_UI, { description: 'Interactive view for roll_dice' }, async () => ({
        contents: [{
          uri: DICE_UI,
          mimeType: RESOURCE_MIME_TYPE,
          text: DICE_HTML,
          _meta: { ui: { csp: { resourceDomains: ['https://unpkg.com'] } } }, // let the sandbox load the App script
        }],
      }));
      return server;
    ```
  </Step>
</Steps>

Run `node --check server.mjs` to catch typos. It prints nothing when the file parses.

### The complete file

For reference, this is `server.mjs` with every edit applied. Everything below `buildServer()` is unchanged from the first quickstart.

<Accordion title="server.mjs with the MCP App added">
  ```js server.mjs theme={null}
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
  import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
  import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
  import { z } from 'zod';

  const DICE_UI = 'ui://quickstart/dice'; // MCP App resources use the ui:// scheme

  const DICE_HTML = `<!DOCTYPE html>
  <html><head><meta charset="utf-8"><meta name="color-scheme" content="light dark"><title>Dice</title>
  <style>
    body { font: 14px system-ui, sans-serif; margin: 0; padding: 12px; }
    #dice { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
    .die { width: 44px; height: 44px; border: 2px solid; border-radius: 10px; display: grid; place-items: center; font-size: 18px; font-weight: 600; }
  </style></head>
  <body>
  <div id="summary">Waiting for a roll…</div>
  <div id="dice"></div>
  <button id="again" disabled>Roll again</button>
  <script type="module">
    // The in-frame client. Loading the prebuilt bundle from a CDN means no bundler in this project.
    import { App } from "https://unpkg.com/@modelcontextprotocol/[email protected]/dist/src/app-with-deps.js";
    const $ = (id) => document.getElementById(id);
    const app = new App({ name: "Dice View", version: "1.0.0" });
    let lastArgs = { sides: 6, count: 1 };
    const show = ({ structuredContent: sc, content }) => {
      $("summary").textContent = sc ? sc.count + "d" + sc.sides + " → total " + sc.total : (content?.[0]?.text ?? "?");
      $("dice").replaceChildren(...(sc?.rolls ?? []).map((n) => Object.assign(document.createElement("div"), { className: "die", textContent: n })));
      $("again").disabled = false;
    };
    // Set handlers before connect() so the first tool-input and tool-result notifications aren't missed.
    app.ontoolinput = ({ arguments: args }) => { if (args) lastArgs = { ...lastArgs, ...args }; };
    app.ontoolresult = show;                     // the host pushes the tool result here; draw it
    $("again").onclick = async () => show(await app.callServerTool({ name: "roll_dice", arguments: lastArgs }));
    await app.connect();                         // handshake with the host
  </script>
  </body></html>`;

  function buildServer() {
    const server = new McpServer({ name: 'quickstart-server', version: '1.0.0' }); // the name Claude sees

    registerAppTool(
      server,
      'roll_dice',
      {
        title: 'Roll dice',
        description: 'Roll `count` dice with `sides` sides each. Returns each roll and the total.',
        inputSchema: {
          sides: z.number().int().min(2).max(100).describe('Number of sides per die (2-100)'),
          count: z.number().int().min(1).max(10).default(1).describe('How many dice to roll (1-10)'),
        },
        _meta: { ui: { resourceUri: DICE_UI } }, // tells the host which resource renders this tool
      },
      async ({ sides, count }) => {
        const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
        const total = rolls.reduce((a, b) => a + b, 0);
        return {
          content: [{ type: 'text', text: `Rolled ${count}d${sides}: [${rolls.join(', ')}] total=${total}` }], // text-only hosts read this
          structuredContent: { sides, count, rolls, total }, // the UI reads this
        };
      },
    );

    registerAppResource(server, 'Dice view', DICE_UI, { description: 'Interactive view for roll_dice' }, async () => ({
      contents: [{
        uri: DICE_UI,
        mimeType: RESOURCE_MIME_TYPE,
        text: DICE_HTML,
        _meta: { ui: { csp: { resourceDomains: ['https://unpkg.com'] } } }, // let the sandbox load the App script
      }],
    }));
    return server;
  }

  // Streamable HTTP: every MCP message arrives as a POST to /mcp. Stateless, so each request gets a fresh server.
  const app = createMcpExpressApp(); // Express app with JSON parsing and localhost protection built in
  app.post('/mcp', async (req, res) => {
    const server = buildServer();
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); // undefined = no sessions
    res.on('close', () => { transport.close(); server.close(); });
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  });
  const notAllowed = (req, res) => res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
  app.get('/mcp', notAllowed);
  app.delete('/mcp', notAllowed);

  const PORT = Number(process.env.PORT ?? 3000);
  app.listen(PORT, '127.0.0.1', (err) => { // Express 5 reports a taken port here instead of throwing
    if (err) { console.error(`failed to listen on ${PORT}: ${err.message}`); process.exit(1); }
    console.log(`quickstart-server listening on http://localhost:${PORT}/mcp`);
  });
  ```
</Accordion>

## Check that the server advertises the UI

A host discovers the UI from the `_meta` on the tool in `tools/list` and loads it from `resources/read`, and your server now returns both. You can see both with `curl` before any host is involved.

<Steps>
  <Step title="Restart the server">
    In the terminal where the server runs, press `Ctrl+C` to stop it, then start it again from the `mcp-quickstart` folder so it picks up your edits:

    ```bash theme={null}
    node server.mjs
    ```

    It prints `quickstart-server listening on http://localhost:3000/mcp` as before.
  </Step>

  <Step title="Check the tool points at the UI">
    In your second terminal, list the tools again:

    ```bash theme={null}
    curl -sS -X POST http://localhost:3000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
    ```

    The `roll_dice` entry now ends with a `_meta` object naming the resource. The helper also writes the same URI under the legacy flat key `ui/resourceUri`. The rest of the entry is shortened to `...` here:

    ```text theme={null}
    data: {"result":{"tools":[{"name":"roll_dice", ... ,"_meta":{"ui":{"resourceUri":"ui://quickstart/dice"},"ui/resourceUri":"ui://quickstart/dice"}}]},"jsonrpc":"2.0","id":2}
    ```
  </Step>

  <Step title="Read the UI resource">
    Fetch the resource the way a host does, by its `ui://` URI:

    ```bash theme={null}
    curl -sS -X POST http://localhost:3000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"ui://quickstart/dice"}}'
    ```

    The reply carries the MCP App MIME type and your HTML as `text`, shortened here after the opening tags:

    ```text theme={null}
    data: {"result":{"contents":[{"uri":"ui://quickstart/dice","mimeType":"text/html;profile=mcp-app","text":"<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\"> ...
    ```
  </Step>

  <Step title="Call the tool and see the structured result">
    Call `roll_dice` once more:

    ```bash theme={null}
    curl -sS -X POST http://localhost:3000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"roll_dice","arguments":{"sides":6,"count":4}}}'
    ```

Cut at 300 lines. The page has the rest.

connectors/building/mcp-apps/transparent-theming Changed · +22 / -18 lines

## Apply the host style variables ## Next steps ## Apply the host's style variables ## Related topics

from line 1
11# Blend your MCP App with Claude's theme
22 
3> Make your widget background transparent and style it with Claude's style variables
3> Make your MCP App's background transparent and style it with Claude's host style variables so it blends into the conversation in light and dark mode.
44 
5Claude renders MCP Apps inside a sandboxed iframe, and every frame between your widget and the chat surface already has a transparent background, so the conversation can show through. When you leave your own background transparent and style text and borders with the host's [style variables](/docs/connectors/building/mcp-apps/design-guidelines#style-variables), your app looks like part of the conversation rather than an embedded box, and it follows the user's light or dark mode automatically.
5Claude renders MCP Apps inside a sandboxed iframe, and every frame between your widget and the chat surface already has a transparent background so the conversation can show through. If you leave your own background transparent and style text and borders with the host's [style variables](/docs/connectors/building/mcp-apps/design-guidelines#style-variables), your app looks like part of the conversation rather than an embedded box. It also follows the user's light or dark mode automatically.
66 
7The snippets on this page assume you have registered a UI resource and created an `App` instance from `@modelcontextprotocol/ext-apps`. See the [SDK Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) if you haven't.
7This page is for developers who already have an MCP App rendering in Claude and want it to match the host theme. The snippets assume you have registered a UI resource and created an `App` instance from `@modelcontextprotocol/ext-apps`. If you haven't, start with the [SDK Quickstart](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html). To match the host theme, [let the host background show through](#let-the-host-background-show-through), then [apply the host style variables](#apply-the-host-style-variables) at runtime.
88 
99## Let the host background show through
1010 
11Three settings on your side keep the transparency intact.
11Every frame Claude places between your widget and the chat surface is already transparent, so transparency holds as long as your own document doesn't paint over it. Leave the body background unpainted, declare `color-scheme`, and request a borderless frame.
1212 
1313### Don't paint a body background
1414 
from line 24
2424 
2525### Declare `color-scheme` in your document head
2626 
27Browsers give iframe documents an opaque canvas backdrop (white in light mode, near-black in dark mode) when the iframe's [`color-scheme`](https://developer.mozilla.org/docs/Web/CSS/color-scheme) differs from the embedding page. Declaring both schemes opts your document into whichever mode the host is in, so the browser drops the backdrop and makes the CSS [`light-dark()`](https://developer.mozilla.org/docs/Web/CSS/color_value/light-dark) values in Claude's tokens resolve correctly:
27Browsers give iframe documents an opaque canvas backdrop, white in light mode and near-black in dark mode, when the iframe's [`color-scheme`](https://developer.mozilla.org/docs/Web/CSS/color-scheme) differs from the embedding page. Declaring both schemes opts your document into whichever mode the host is in, so the browser drops the backdrop and makes the CSS [`light-dark()`](https://developer.mozilla.org/docs/Web/CSS/color_value/light-dark) values in Claude's tokens resolve correctly:
2828 
2929```html theme={null}
3030<meta name="color-scheme" content="light dark" />
from line 58
5858}));
5959```
6060 
61## Apply the host's style variables
61## Apply the host style variables
6262 
6363Claude passes a [`hostContext`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiHostContext.html) object to your widget during the [`connect()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#connect) handshake. The fields relevant to theming are:
6464 
from line 72
7272 
7373### Read `hostContext` and listen for changes
7474 
75The [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class exposes the initial context via [`getHostContext()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#gethostcontext) once `connect()` resolves, and delivers subsequent updates (such as the user toggling dark mode) through the [`hostcontextchanged`](https://modelcontextprotocol.github.io/ext-apps/api/types/app.AppEventMap.html) event. Register the listener before you connect so you don't miss an early update.
75The [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class exposes the initial context via [`getHostContext()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#gethostcontext) once `connect()` resolves, and delivers subsequent updates, such as the user toggling dark mode, through the [`hostcontextchanged`](https://modelcontextprotocol.github.io/ext-apps/api/types/app.AppEventMap.html) event. Register the listener before you connect so you don't miss an early update.
7676 
77The SDK provides three helpers that do the DOM work for you, plus React hooks that wrap them:
77The SDK provides helpers that do the DOM work for you, and React hooks that wrap them:
7878 
79* [`applyDocumentTheme(theme)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyDocumentTheme.html) sets `<html data-theme>` and the root `color-scheme`, so `[data-theme="dark"]` selectors and `light-dark()` values resolve correctly.
80* [`applyHostStyleVariables(variables)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostStyleVariables.html) writes every entry in `styles.variables` onto `:root` as a CSS custom property.
81* [`applyHostFonts(fontCss)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostFonts.html) injects the host's `@font-face` rules once.
82* [`useApp(options)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) creates and connects the `App` instance for you in React.
83* [`useHostStyles(app, hostContext)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useHostStyles.html) applies all of the above and re-applies on `hostcontextchanged`.
79* [`applyDocumentTheme(theme)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyDocumentTheme.html) sets `<html data-theme>` and the root `color-scheme`, so `[data-theme="dark"]` selectors and `light-dark()` values resolve correctly
80* [`applyHostStyleVariables(variables)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostStyleVariables.html) writes every entry in `styles.variables` onto `:root` as a CSS custom property
81* [`applyHostFonts(fontCss)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostFonts.html) injects the host's `@font-face` rules once
82* [`useApp(options)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) creates and connects the `App` instance for you in React
83* [`useHostStyles(app, hostContext)`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useHostStyles.html) applies the theme, variables, and fonts and re-applies on `hostcontextchanged`
8484 
85Keep the `<meta name="color-scheme">` tag from the previous section even though `applyDocumentTheme` also sets `color-scheme` at runtime. The tag covers the first paint before your script runs and prevents an opaque-backdrop flash.
85Keep the `<meta name="color-scheme">` tag in your document head even though `applyDocumentTheme` also sets `color-scheme` at runtime. The tag covers the first paint before your script runs and prevents an opaque-backdrop flash.
8686 
87This example applies the theme, variables, and fonts on connect and again on every host context change:
88 
8789<CodeGroup>
8890 ```ts TypeScript theme={null}
8991 import {
from line 148
146148 
147149### Allow the host font origin in your CSP
148150 
149For `applyHostFonts` to load the `@font-face` files, your resource's [`_meta.ui.csp`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#csp) allowlist must include `https://assets.claude.ai` in [`resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) (shown in the [`registerAppResource` snippet above](#request-a-borderless-frame)). `resourceDomains` also adds the listed origins to `script-src` and `style-src`, so keep it to origins you trust to serve executable code; prefer bundling third-party fonts into your widget rather than allowlisting public CDNs.
151For `applyHostFonts` to load the `@font-face` files, your resource's [`_meta.ui.csp`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#csp) allowlist must include `https://assets.claude.ai` in [`resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains), as the [`registerAppResource` snippet](#request-a-borderless-frame) shows. `resourceDomains` also adds the listed origins to `script-src` and `style-src`, so keep it to origins you trust to serve executable code. Prefer bundling third-party fonts into your widget rather than allowlisting public CDNs.
150152 
151## Related topics
153## Next steps
152154 
153* [Design guidelines: Style variables](/docs/connectors/building/mcp-apps/design-guidelines#style-variables) and [Visual design](/docs/connectors/building/mcp-apps/design-guidelines#visual-design) for the full variable palette and usage guidance.
154* [SDK API reference](https://modelcontextprotocol.github.io/ext-apps/api/index.html) for `App`, `McpUiHostContext`, and `McpUiResourceMeta`.
155* [Style variables](/docs/connectors/building/mcp-apps/design-guidelines#style-variables): the full variable palette in the design guidelines
156* [Visual design](/docs/connectors/building/mcp-apps/design-guidelines#visual-design): usage guidance for color, typography, and spacing in the design guidelines
157* [Supersede older widget instances](/docs/connectors/building/mcp-apps/instance-supersession): keep only the newest copy of your widget active in a conversation
158* [SDK API reference](https://modelcontextprotocol.github.io/ext-apps/api/index.html): `App`, `McpUiHostContext`, and `McpUiResourceMeta`
155159 

connectors/building/mcp-apps/troubleshooting Changed · +55 / -31 lines

# Troubleshoot MCP Apps ## Open developer tools ### Claude Desktop ## Fix common problems ### Tool call appears but the app is invisible #### Missing `app.connect()` call #### Iframe has zero height ### App doesn't render when tool results are large ### Assets or API requests fail only on iOS ### `ui.domain` validation fails ## Next steps # Troubleshooting MCP Apps ## Using developer tools ### Desktop ## Problem: Tool call appears but the app is invisible ### Missing `app.connect()` call ### Iframe has zero height ## Problem: App doesn't render when tool results are large ## Problem: Assets or API requests fail only on iOS ## Problem: ui.domain validation fails

from line 1
1# Troubleshooting MCP Apps
1# Troubleshoot MCP Apps
22 
3> Debug and resolve common issues with MCP Apps
3> Debug MCP Apps in Claude with developer tools on desktop and iOS, and fix invisible apps, large tool results, iOS-only request failures, and ui.domain errors.
44 
5## Using developer tools
5When an MCP App doesn't render or load correctly in Claude, the tool call usually still appears in the conversation, and the cause is in how the app connects, sizes itself, receives its data, or loads its assets. This page is for developers debugging their own MCP App. [Open the developer tools](#open-developer-tools) in Claude Desktop or on iOS to inspect the app's iframe, then match what you see against the [common problems](#fix-common-problems).
66 
7### Desktop
7## Open developer tools
88 
9Claude Desktop and the Claude iOS app both let you inspect a running MCP App with browser developer tools.
10 
11### Claude Desktop
12 
913Claude Desktop's Developer Tools can help you debug MCP Apps. To use them:
1014 
111. Open **Help > Troubleshooting** and click **Enable Developer Mode**. A new **Developer** menu appears in the menu bar.
122. Open Developer Tools by pressing `Cmd+Option+I` (Mac) or `Ctrl+Shift+I` (Windows)
133. Inspect the tool call element and look for an iframe nested inside another iframe. Your app will be loaded as the content of the inner iframe.
15<Steps>
16 <Step title="Enable Developer Mode">
17 Open **Help > Troubleshooting** and click **Enable Developer Mode**. A new **Developer** menu appears in the menu bar.
18 </Step>
1419 
20 <Step title="Open Developer Tools">
21 Open Developer Tools by pressing `Cmd+Option+I` on Mac or `Ctrl+Shift+I` on Windows.
22 </Step>
23 
24 <Step title="Find your app's iframe">
25 Inspect the tool call element and look for an iframe nested inside another iframe. Your app is loaded as the content of the inner iframe.
26 </Step>
27</Steps>
28 
1529<Tip>From the **Developer** menu, select **Reload MCP Configuration** after editing your `claude_desktop_config.json` to apply changes without restarting.</Tip>
1630 
1731### iOS
1832 
19On iOS, the Claude app renders your MCP app inside a `WKWebView`. You can inspect it from a connected Mac using Safari's Web Inspector—see Apple's guide to [inspecting iOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios) for setup. Once connected, the Claude web view appears under your device in Safari's **Develop** menu, and you can use the console, network panel, and element inspector just as you would on desktop.
33On iOS, the Claude app renders your MCP App inside a `WKWebView`. You can inspect it from a connected Mac using Safari's Web Inspector. Follow Apple's guide to [inspecting iOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios) for setup. Once connected, the Claude web view appears under your device in Safari's **Develop** menu, and you can use the console, network panel, and element inspector as you would on desktop.
2034 
21## Problem: Tool call appears but the app is invisible
35## Fix common problems
2236 
23This is the most common issue when developing MCP Apps. Check these two causes:
37These are the problems developers hit most often when an MCP App doesn't render or load correctly in Claude, each with its cause and fix.
2438 
25### Missing `app.connect()` call
39### Tool call appears but the app is invisible
2640 
27Your app must call [`app.connect()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#connect) (Vanilla JS) or [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) (React) to establish communication with Claude Desktop.
41An invisible app under a visible tool call is the most common issue when developing MCP Apps. The cause is usually a missing `app.connect()` call or an iframe with zero height.
2842 
43#### Missing `app.connect()` call
44 
45Your app must call [`app.connect()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#connect) in vanilla JS or [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) in React to establish communication with Claude Desktop. Register your handlers before connecting:
46 
2947<CodeGroup>
3048 ```javascript Vanilla JS theme={null}
3149 import { App } from "@modelcontextprotocol/ext-apps";
from line 76
5876 ```
5977</CodeGroup>
6078 
61<Warning>Event handlers like `app.ontoolinput` and `app.ontoolresult` won't be invoked until the app is connected.</Warning>
79<Warning>Event handlers like `app.ontoolinput` and `app.ontoolresult` aren't invoked until the app is connected.</Warning>
6280 
63### Iframe has zero height
81#### Iframe has zero height
6482 
6583Your app needs a non-zero height to be visible. A zero height can occur if:
6684 
from line 87
6987 
7088Check that your root element has explicit dimensions or content that gives it height.
7189 
72## Problem: App doesn't render when tool results are large
90### App doesn't render when tool results are large
7391 
74When a tool result exceeds approximately 150,000 characters and Claude's code execution sandbox is active, the result is written to the sandbox filesystem instead of being passed inline to the conversation. Your app receives a pointer to the stored file rather than the structured content it needs, so it never hydrates.
92When a tool result exceeds approximately 150,000 characters and Claude's code execution sandbox is active, Claude writes the result to the sandbox filesystem instead of passing it inline to the conversation. Your app receives a pointer to the stored file rather than the structured content it needs, so it never hydrates.
7593 
76<Note>This \~150,000-character threshold is specific to Claude.ai and Claude Desktop. Claude Code uses a separate 25,000-token default limit configurable via `MAX_MCP_OUTPUT_TOKENS`.</Note>
94<Note>This \~150,000-character threshold is specific to claude.ai and Claude Desktop. Claude Code uses a separate 25,000-token default limit, configurable through `MAX_MCP_OUTPUT_TOKENS`.</Note>
7795 
78To avoid this, keep initial tool result payloads lean:
96To stay under the threshold, keep initial tool result payloads lean:
7997 
80* **Paginate large results.** Return a summary or the first page of data, and let the user request more through follow-up interactions.
81* **Fetch details on demand.** Use app-initiated tool calls to load additional data from within your widget as the user explores, rather than returning everything upfront.
82* **Defer heavy content.** If your data includes large blobs—full document text, base64-encoded images, extensive logs—return identifiers or previews in the initial result and provide a separate tool to retrieve the full content when needed.
98* **Paginate large results**: return a summary or the first page of data, and let the user request more through follow-up interactions
99* **Fetch details on demand**: use app-initiated tool calls to load additional data from within your widget as the user explores, rather than returning everything upfront
100* **Defer heavy content**: if your data includes large blobs such as full document text, base64-encoded images, or extensive logs, return identifiers or previews in the initial result and provide a separate tool to retrieve the full content when needed
83101 
84## Problem: Assets or API requests fail only on iOS
102### Assets or API requests fail only on iOS
85103 
86104If your app loads on desktop and web but fails to fetch scripts, images, or API data on iOS, check whether your server, CDN, or WAF is gating access on the `Referer` header.
87105 
88WebKit on iOS—both Safari and in the Claude iOS app—omits the `Referer` header on cross-origin subresource requests as part of its tracking prevention (WebKit bugs [206521](https://bugs.webkit.org/show_bug.cgi?id=206521) and [179053](https://bugs.webkit.org/show_bug.cgi?id=179053#c8)). A server that requires a `Referer` to allow the request will reject iOS traffic even though the same app works elsewhere.
106WebKit on iOS, in both Safari and the Claude iOS app, omits the `Referer` header on cross-origin subresource requests as part of its tracking prevention, per WebKit bugs [206521](https://bugs.webkit.org/show_bug.cgi?id=206521) and [179053](https://bugs.webkit.org/show_bug.cgi?id=179053#c8). A server that requires a `Referer` to allow the request rejects iOS traffic even though the same app works elsewhere.
89107 
90**Fix:** Allowlist on the `Origin` header instead, which WebKit does send. Requests from your app carry an `Origin` of `{hash}.claudemcpcontent.com`—see [Domain handling](/docs/connectors/building/mcp-apps/cross-compatibility#domain-handling) to compute the hash for your server URL. Configure your infrastructure to allow requests whose `Origin` matches `*.claudemcpcontent.com` and return a corresponding `Access-Control-Allow-Origin` header.
108To fix the iOS failures, allowlist on the `Origin` header instead of `Referer`, because WebKit does send `Origin`. Requests from your app carry an `Origin` of `{hash}.claudemcpcontent.com`, and [Set `ui.domain` for Claude](/docs/connectors/building/mcp-apps/getting-started#set-ui-domain-for-claude) shows how to compute the hash for your server URL. Configure your infrastructure to allow requests whose `Origin` matches `*.claudemcpcontent.com` and return a corresponding `Access-Control-Allow-Origin` header.
91109 
92<Note>This applies to requests your app makes directly from the user's device—loading bundles, images, or calling your own API from client-side code. MCP tool calls are proxied through Claude's backend and egress from Anthropic's published IP ranges, not the user's device.</Note>
110<Note>The missing `Referer` header affects requests your app makes directly from the user's device, such as loading bundles and images or calling your own API from client-side code. MCP tool calls are proxied through Claude's backend and egress from Anthropic's published IP ranges, not the user's device.</Note>
93111 
94## Problem: ui.domain validation fails
112### `ui.domain` validation fails
95113 
96Setting [`_meta.ui.domain`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#domain) on your resource opts your app into a stable sandbox origin, which you need if your app runs its own OAuth flow. Claude validates the value against your connector URL and shows an `Invalid ui.domain format` or `ui.domain mismatch` error instead of rendering the app when validation fails.
114Setting [`_meta.ui.domain`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceMeta.html#domain) on your resource opts your app into a stable sandbox origin, which you need if your app runs its own OAuth flow. Claude validates the value against your connector URL, and when validation fails it shows an `Invalid ui.domain format` or `ui.domain mismatch` error instead of rendering the app.
97115 
98116The value must be exactly `{hash}.claudemcpcontent.com`, where `{hash}` is the first 32 hexadecimal characters of the SHA-256 digest of your full connector URL. Compute it by running this command with your own URL:
99117 
from line 119
101119node -e 'const yourServerUrl = "https://example.com/mcp"; console.log(require("crypto").createHash("sha256").update(yourServerUrl).digest("hex").slice(0,32) + ".claudemcpcontent.com")'
102120```
103121 
104Common causes of a mismatch:
122A mismatch usually has one of these causes:
105123 
106* **The URL you hashed differs from the URL Claude connects to.** The hash covers the full URL string including scheme, path, and any trailing slash, so `https://example.com/mcp` and `https://example.com/mcp/` produce different values. Hash the exact URL configured in **Customize > Connectors**.
107* **The connector is local (stdio).** Local connectors have no URL to hash, so `ui.domain` is not available for them. Remove the field, or deploy the server as a remote connector to use a stable origin.
124* **The URL you hashed differs from the URL Claude connects to**: the hash covers the full URL string including scheme, path, and any trailing slash, so `https://example.com/mcp` and `https://example.com/mcp/` produce different values. Hash the exact URL configured in **Customize > Connectors**
125* **The connector is a local stdio server**: local connectors have no URL to hash, so `ui.domain` isn't available for them. Remove the field, or deploy the server as a remote connector to use a stable origin
108126 
109See [Domain handling](/docs/connectors/building/mcp-apps/cross-compatibility#domain-handling) for how the origin is used across platforms.
127[Set `ui.domain` for Claude](/docs/connectors/building/mcp-apps/getting-started#set-ui-domain-for-claude) explains how the origin is used across platforms.
128 
129## Next steps
130 
131* [Set `ui.domain` for Claude](/docs/connectors/building/mcp-apps/getting-started#set-ui-domain-for-claude): compute the sandbox origin Claude expects for your app
132* [Design guidelines](/docs/connectors/building/mcp-apps/design-guidelines#mobile-guidelines): mobile layout, safe areas, and sizing rules that prevent clipped or invisible content
133* [Get started with MCP Apps](/docs/connectors/building/mcp-apps/getting-started#build-your-own-mcp-app): SDK quickstart, examples, and agent skills
110134 

connectors/building/mcpb Changed · +76 / -70 lines

## Decide when to build an MCPB ### Choose between MCPB and a remote connector ## Build the bundle ### Choose a language ### Platform support ### Create and pack the bundle ## Configure manifest.json ### Add an icon ### User configuration ## Distribute your MCPB ### Understand how users install your MCPB ## Get help with MCPB ## Related resources ### MCPB framework ### MCP protocol ### Claude Desktop ## Next steps ## What is an MCPB? ## Local (MCPB) vs remote: which to build ## Choose a language ## Platform support ## Quickstart ## manifest.json ## Add an icon ## User configuration ## How users install your MCPB ## Resources ## Get help ## Ready for distribution

from line 1
11# Build a desktop extension with MCPB
22 
3> Package a local MCP server as a single-click .mcpb install for Claude Desktop
3> Package a local MCP server as a single-click .mcpb install for Claude Desktop: when to build one, the CLI quickstart, manifest.json, and how users install it.
44 
5<Note>
6 MCPB is the secondary distribution path. Remote MCP servers are recommended for directory listing—see [what to build](/docs/connectors/building/what-to-build).
7</Note>
5An MCP Bundle (`.mcpb`) is a zip archive containing a local MCP server and a `manifest.json`, which Claude Desktop installs in a single click the way a browser installs an extension. The server runs on the user's machine over stdio, so it can reach local files, locally installed tools, and systems behind the user's firewall without any cloud infrastructure.
86 
9This guide covers building an MCP Bundle (`.mcpb`) for internal use, private distribution, or as a foundation for [submission to the Connectors Directory](/docs/connectors/building/submission).
7This page is for developers packaging a local MCP server for Claude Desktop, whether for internal use or private distribution. It covers when to choose MCPB over a remote server, building and packing the bundle, the manifest, and how users install the result.
108 
11## What is an MCPB?
9For a directory listing, build a remote MCP server, which reaches people on every surface, or include the local server in a plugin, as [Decide what to include in your plugin](/docs/connectors/building/what-to-build) explains.
1210 
13An `.mcpb` file is a zip archive containing a local MCP server and a `manifest.json`. It enables single-click installation in Claude Desktop, similar to a browser extension.
11<Note>
12 * If you're building a remote server, see [Build an MCP server for Claude](/docs/connectors/building/index)
13 * If you're deploying desktop extensions across a Team or Enterprise organization, see [Install a local connector in the desktop app](/docs/connectors/custom/add-unlisted#install-a-local-connector-in-the-desktop-app)
14</Note>
1415 
15Key characteristics:
16## Decide when to build an MCPB
1617 
18An MCPB has these characteristics:
19 
1720* Runs locally on the user's machine
1821* Communicates via stdio transport
1922* Bundles all dependencies
from line 23
2023* Works offline
2124* No OAuth required
2225 
26MCPBs run on the user's machine via stdio with access to local and internal resources. Remote connectors run on your servers via HTTPS and are accessed through Anthropic's infrastructure. Organizations commonly build MCPBs as secure proxies to internal MCP servers, for internal documentation access, and to connect development tools while preserving their security architecture.
27 
2328See the [MCPB repository](https://github.com/modelcontextprotocol/mcpb) for the complete specification and the [Desktop Extensions blog post](https://www.anthropic.com/engineering/desktop-extensions) for an architecture overview.
2429 
25## Local (MCPB) vs remote: which to build
30### Choose between MCPB and a remote connector
2631 
27| Choose MCPB when you need | Choose a remote connector when you need |
28| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
29| Access to systems behind your firewall (JIRA, Confluence, internal wikis, private databases) | Cloud services and public APIs with centralized infrastructure |
30| Authentication via existing SSO and browser sessions, no token management | OAuth flows with server-side token management |
31| Zero-trust compliance inside corporate network boundaries | Distribution across Claude on web, mobile, and desktop |
32| Direct filesystem access for code editing and Git operations | Centralized updates pushed to all users |
33| Integration with locally installed tools (Docker, IDEs, databases) | Public-facing integrations used by multiple organizations |
34| Hardware integration and desktop application control | |
35| Privacy-sensitive operations that should not leave the user's machine | |
36| One-click install with bundled Node.js runtime, no dependencies to manage | |
37| No cloud infrastructure, VPN configuration, or firewall rules | |
38| Organization-level admin controls (custom uploads, allowlists) | |
39| Full control over authentication, authorization, and audit logs | |
32The table lists the needs that point to each option.
4033 
41**Key difference:** MCPBs run on the user's machine via stdio with access to local and internal resources. Remote connectors run on your servers via HTTPS and are accessed through Anthropic's infrastructure.
34| Choose MCPB when you need | Choose a remote connector when you need |
35| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
36| Access to systems behind your firewall, such as your issue tracker, internal wikis, and private databases | Cloud services and public APIs with centralized infrastructure |
37| Authentication via existing SSO and browser sessions, no token management | OAuth flows with server-side token management |
38| Zero-trust compliance inside corporate network boundaries | Distribution across Claude on web, mobile, and desktop |
39| Direct filesystem access for code editing and Git operations | Centralized updates pushed to all users |
40| Integration with locally installed tools, such as Docker, IDEs, and databases | Public-facing integrations used by multiple organizations |
41| Hardware integration and desktop application control | |
42| Privacy-sensitive operations that should not leave the user's machine | |
43| One-click install with bundled Node.js runtime, no dependencies to manage | |
44| No cloud infrastructure, VPN configuration, or firewall rules | |
45| Organization-level admin controls, such as custom uploads and allowlists | |
46| Full control over authentication, authorization, and audit logs | |
4247 
43Organizations commonly build MCPBs as secure proxies to internal MCP servers, for internal documentation access, and to connect development tools while preserving their security architecture.
48## Build the bundle
4449 
45For remote connector guidance, see [building custom connectors](/docs/connectors/building/index).
50You write a stdio MCP server, generate a manifest with the MCPB CLI, and pack both into a `.mcpb` file. Choose the language and target platforms before you start.
4651 
47## Choose a language
52### Choose a language
4853 
49Node.js is strongly recommended:
54Node.js is strongly recommended, for these reasons:
5055 
51* Ships with Claude Desktop on macOS and Windows, so users need no separate runtime
56* Included with Claude Desktop on macOS and Windows, so users need no separate runtime
5257* Best compatibility and reliability with Claude Desktop
5358* Extensive MCP SDK support
5459 
55## Platform support
60### Platform support
5661 
5762Claude Desktop runs on macOS (`darwin`) and Windows (`win32`). Specify supported platforms in the `compatibility` section of your `manifest.json`. Test on both platforms even if you primarily develop on one.
5863 
5964See the [manifest spec compatibility section](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md#compatibility) for platform and runtime requirement details.
6065 
61## Quickstart
66### Create and pack the bundle
6267 
68The MCPB CLI generates the manifest and packs the bundle.
69 
6370<Steps>
6471 <Step title="Install the MCPB CLI">
6572 ```bash theme={null}
from line 101
94101 Before distributing your MCPB, review the testing and best-practices guidance in the MCPB README to ensure quality.
95102</Warning>
96103 
97## manifest.json
104## Configure manifest.json
98105 
99The `manifest.json` file is required metadata describing what your MCPB does, how to run it, which tools it provides, and what configuration it needs.
106The `manifest.json` file is required metadata describing what your MCPB does, how to run it, which tools it provides, and what configuration it needs. These references document it:
100107 
101| Reference | |
108| Reference | Contents |
102109| ---------------------------------------------------------------------------------------- | --------------------------- |
103110| [MCPB Manifest Spec](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md) | Full schema with all fields |
104111| [Example manifests](https://github.com/modelcontextprotocol/mcpb/tree/main/examples) | Real-world implementations |
105112| [CLI documentation](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md) | Command reference |
106113 
107## Add an icon
114### Add an icon
108115 
109Icons are optional but recommended. Place `icon.png` in your bundle root and reference it in `manifest.json`.
116Icons are optional but recommended. Place `icon.png` in your bundle root and reference it in `manifest.json`. The icon must meet these requirements:
110117 
111| Requirement | Value |
112| ----------- | ----------------------------------------- |
113| File name | `icon.png` (or a custom path) |
114| Size | 512×512px recommended (minimum 256×256px) |
115| Format | PNG with transparency |
116| Location | Bundle root or specified path |
118| Requirement | Value |
119| ----------- | ---------------------------------------- |
120| File name | `icon.png`, or a custom path |
121| Size | 512×512px recommended, 256×256px minimum |
122| Format | PNG with transparency |
123| Location | Bundle root or specified path |
117124 
118You can also provide multiple icon variants for different sizes and themes (light/dark mode). See the [manifest spec icons section](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md#icons) for variant syntax and best practices.
125You can also provide multiple icon variants for different sizes and for light and dark themes. See the [manifest spec icons section](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md#icons) for variant syntax and best practices.
119126 
120## User configuration
127### User configuration
121128 
122129Define a `user_config` section in `manifest.json` and Claude Desktop automatically generates a settings UI for your extension. The [manifest spec user configuration section](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md#user-configuration) covers the full schema, configuration types, validation constraints, sensitive-data handling, and multi-select patterns.
123130 
124## How users install your MCPB
131## Distribute your MCPB
125132 
126Users can install three ways:
133Users install the `.mcpb` file themselves in Claude Desktop. Desktop extension listings in the directory are deprecated, and the directory no longer accepts MCPB submissions. To distribute a local MCP server through the directory, include it in a [plugin](/docs/plugins/overview).
127134 
1281. **Double-click** the `.mcpb` file
1292. **Drag and drop** the `.mcpb` file into the Claude Desktop window
1303. **Settings**: Settings → Extensions → Advanced settings → Install Extension… → select the `.mcpb` file
135### Understand how users install your MCPB
131136 
132All three open an installation UI where the user reviews extension details and permissions, configures required settings, grants permissions, and completes installation. Installation is per-user; each user installs separately on their own system.
137Users can install your MCPB in any of these ways:
133138 
134For the end-user installation experience and Team/Enterprise admin controls (organization management, allowlists, policy configuration), see [Getting Started with Local MCP Servers on Claude Desktop](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop).
139* Double-click the `.mcpb` file
140* Drag and drop the `.mcpb` file into the Claude Desktop window
141* In Claude Desktop, go to **Settings > Extensions > Advanced settings > Install Extension…** and select the `.mcpb` file
135142 
136## Resources
143Each of these install methods opens an installation UI where the user reviews extension details and permissions, configures required settings, grants permissions, and completes installation. Installation is per-user, so each user installs separately on their own system.
137144 
138**MCPB framework**
145For the end-user installation experience and Team and Enterprise admin controls, such as organization management, allowlists, and policy configuration, see [Getting Started with Local MCP Servers on Claude Desktop](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop).
139146 
147## Get help with MCPB
148 
149* [MCPB GitHub issues](https://github.com/modelcontextprotocol/mcpb/issues): bug reports and feature requests
150* [MCP specification repo](https://github.com/modelcontextprotocol/modelcontextprotocol): protocol questions
151* [Claude support](https://support.claude.com/en/articles/9015913-how-to-get-support): general Claude Desktop support
152 
153## Related resources
154 
155These external references cover the MCPB format, the MCP protocol, and Claude Desktop.
156 
157### MCPB framework
158 
140159* [MCPB repository](https://github.com/modelcontextprotocol/mcpb): complete specification and tools
141160* [MCPB Manifest Spec](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md): full manifest schema
142161* [MCPB CLI documentation](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md): command reference
143162* [MCPB examples](https://github.com/modelcontextprotocol/mcpb/tree/main/examples): reference implementations
144163 
145**MCP protocol**
164### MCP protocol
146165 
147166* [MCP specification](https://modelcontextprotocol.io/docs/getting-started/intro): protocol documentation
148167* [MCP quickstart](https://modelcontextprotocol.io/docs/develop/build-server): getting-started guide
from line 168
149168* [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk): Node.js implementation
150169* [Python SDK](https://github.com/modelcontextprotocol/python-sdk): Python implementation
151170 
152**Claude Desktop**
171### Claude Desktop
153172 
154173* [Release notes](https://support.claude.com/en/articles/12138966-release-notes): version updates
155174* [Desktop Extensions blog](https://www.anthropic.com/engineering/desktop-extensions): architecture overview
156175 
157## Get help
176## Next steps
158177 
159* [MCPB GitHub issues](https://github.com/modelcontextprotocol/mcpb/issues): bug reports and feature requests
160* [MCP specification repo](https://github.com/modelcontextprotocol/modelcontextprotocol): protocol questions
161* [Claude support](https://support.claude.com/en/articles/9015913-how-to-get-support): general Claude Desktop support
162 
163Check repository discussions for community Q\&A, follow release notes for updates, and review the examples for implementation patterns.
164 
165## Ready for distribution
166 
167If you have a working MCPB and want broader distribution and discoverability, submit it to the Connectors Directory. See [submitting to the directory](/docs/connectors/building/submission) for requirements including:
168 
169* Mandatory tool annotations for all tools
170* Privacy policy requirements
171* Working examples that exercise each tool
172* Test credentials where applicable
173* The complete submission process and review timeline
178* [Install a local connector in the desktop app](/docs/connectors/custom/add-unlisted#install-a-local-connector-in-the-desktop-app): deploy local MCP servers for Claude Desktop across a Team or Enterprise organization
179* [Submit your plugin](/docs/plugins/submit): list a plugin that includes your local MCP server in the directory
174180 

connectors/building/quickstart New page · 292 lines, new page

# Build your first MCP server for Claude ## Before you begin ## Create the project ## Write the server ## Run and test the server ## Connect the server to Claude Code ## Put the server in front of claude.ai users ## Clean up ## Next steps

A whole new page. There's nothing to diff it against, so here is what it says.

# Build your first MCP server for Claude

> Build a minimal MCP server with one tool in JavaScript, run it on your machine, and connect it to Claude Code so Claude calls your tool.

An MCP server is a program that gives Claude tools it can call, and people who use Claude see it as a connector. In this quickstart you write one in about 50 lines of JavaScript with no build step: a server with a single `roll_dice` tool that runs on your machine, which you then connect to [Claude Code](https://code.claude.com/docs/en/setup), Anthropic's command-line coding tool, and watch Claude call. At the end you have a working server you understand line by line, ready to grow into your own product's tools, wrap in a [plugin](/docs/plugins/quickstart), or host for people on claude.ai.

This quickstart is for developers who haven't built an MCP server before and want to see every piece working before they add authentication, hosting, and real tools.

<Note>
  * If you already have a server and want to know what Claude's client supports, see [Build an MCP server for Claude](/docs/connectors/building/index)
  * If your server is running and you want to try it in Claude, see [Test your connector](/docs/connectors/building/testing)
  * If you want to package skills and an existing connector rather than write a server, see [Build your first plugin](/docs/plugins/quickstart)
</Note>

## Before you begin

Check that you have each of these:

* **Node.js 18 or later**: run `node --version` to check. The steps on this page were verified with Node.js 22
* **npm**: included with Node.js
* **Claude Code**: [install Claude Code](https://code.claude.com/docs/en/setup) and sign in. You use it in the last part to connect to the server and call the tool, and `claude -p` needs a signed-in account to send the prompt
* **Two terminal windows**: one keeps the server running while you run commands in the other

## Create the project

The project is a folder with a `package.json` and two dependencies: the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), which also works from plain JavaScript, and `zod`, which the SDK uses to describe tool inputs.

<Steps>
  <Step title="Make a folder for the server">
    In your terminal, create a folder named `mcp-quickstart` and move into it. Every later command on this page runs from this folder:

    ```bash theme={null}
    mkdir mcp-quickstart
    cd mcp-quickstart
    ```
  </Step>

  <Step title="Create package.json">
    Create a default `package.json`, then set `"type": "module"` in it so Node.js treats the project's files as ES modules:

    ```bash theme={null}
    npm init -y
    npm pkg set type=module
    ```
  </Step>

  <Step title="Install the SDK">
    Install the SDK and `zod`. You don't install a web framework separately, because the SDK depends on Express and gives you a ready-made app for it:

    ```bash theme={null}
    npm install @modelcontextprotocol/sdk zod
    ```

    To confirm what installed, run `npm ls --depth=0`. The output lists the two packages and their versions:

    ```text theme={null}
    [email protected] /path/to/mcp-quickstart
    +-- @modelcontextprotocol/[email protected]
    `-- [email protected]
    ```

    This page was verified with those versions.
  </Step>
</Steps>

## Write the server

The whole server is one file. Create `server.mjs` in the `mcp-quickstart` folder and paste in the code below.

The highlighted lines matter most: the `McpServer` that Claude connects to, the `registerTool` call that describes `roll_dice` and its inputs, and the `/mcp` route that receives each request over Streamable HTTP.

```js server.mjs {7,10-19,31-38} theme={null}
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import { z } from 'zod';

function buildServer() {
  const server = new McpServer({ name: 'quickstart-server', version: '1.0.0' }); // the name Claude sees

  // One tool: its name, the description Claude reads to decide when to call it, and its inputs.
  server.registerTool(
    'roll_dice',
    {
      title: 'Roll dice',
      description: 'Roll `count` dice with `sides` sides each. Returns each roll and the total.',
      inputSchema: {
        sides: z.number().int().min(2).max(100).describe('Number of sides per die (2-100)'),
        count: z.number().int().min(1).max(10).default(1).describe('How many dice to roll (1-10)'),
      },
    },
    // Runs when Claude calls the tool. The text you return is what Claude reads.
    async ({ sides, count }) => {
      const rolls = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * sides));
      const total = rolls.reduce((a, b) => a + b, 0);
      return { content: [{ type: 'text', text: `Rolled ${count}d${sides}: [${rolls.join(', ')}] total=${total}` }] };
    },
  );
  return server;
}

// Streamable HTTP: every MCP message arrives as a POST to /mcp. Stateless, so each request gets a fresh server.
const app = createMcpExpressApp(); // Express app with JSON parsing and localhost protection built in
app.post('/mcp', async (req, res) => {
  const server = buildServer();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); // undefined = no sessions
  res.on('close', () => { transport.close(); server.close(); });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});
const notAllowed = (req, res) => res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
app.get('/mcp', notAllowed);
app.delete('/mcp', notAllowed);

const PORT = Number(process.env.PORT ?? 3000);
app.listen(PORT, '127.0.0.1', (err) => { // Express 5 reports a taken port here instead of throwing
  if (err) { console.error(`failed to listen on ${PORT}: ${err.message}`); process.exit(1); }
  console.log(`quickstart-server listening on http://localhost:${PORT}/mcp`);
});
```

Each part of the file has one job:

* **`McpServer`**: the object that speaks MCP. Its `name` and `version` are what a client sees when it connects
* **`registerTool`**: adds `roll_dice`. A tool is a function your server offers to Claude, and Claude decides when to call it from the `description`. The `inputSchema` tells the client that `sides` is a whole number from 2 to 100 and that `count` is optional and defaults to 1
* **The handler**: rolls the dice and returns one text block. Whatever you put in `content` is the tool result Claude reads
* **The `/mcp` route**: the SDK's Express app listens on `127.0.0.1:3000`, and each POST to `/mcp` is one MCP request. The server keeps no session between requests, which is the simplest shape and enough for tools like this one
* **The `listen` callback**: prints the address when the server is up, or the reason and exits if the port is already in use

## Run and test the server

Before you involve Claude, start the server and send it MCP requests yourself with `curl`, so you know it works on its own.

<Steps>
  <Step title="Start the server">
    In your first terminal, from the `mcp-quickstart` folder, start the server:

    ```bash theme={null}
    node server.mjs
    ```

    It prints the address it's listening on and keeps running:

    ```text theme={null}
    quickstart-server listening on http://localhost:3000/mcp
    ```

    Leave this terminal open. If you see `failed to listen on 3000` instead, another program is using the port. Stop that program and run the command again.
  </Step>

  <Step title="List the server's tools">
    In your second terminal, ask the server what tools it has. This is the same `tools/list` request Claude sends when it connects. The `Accept` header is required, and the server answers `406 Not Acceptable` without it:

    ```bash theme={null}
    curl -sS -X POST http://localhost:3000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
    ```

    The reply is one server-sent event whose `data` line lists `roll_dice` with the title, description, and input schema you wrote. The `inputSchema` object is shortened to `...` here:

    ```text theme={null}
    event: message
    data: {"result":{"tools":[{"name":"roll_dice","title":"Roll dice","description":"Roll `count` dice with `sides` sides each. Returns each roll and the total.","inputSchema":{...},"execution":{"taskSupport":"forbidden"}}]},"jsonrpc":"2.0","id":2}
    ```
  </Step>

  <Step title="Call the tool">
    Call `roll_dice` directly with three 20-sided dice, which is the `tools/call` request Claude sends when it uses the tool:

    ```bash theme={null}
    curl -sS -X POST http://localhost:3000/mcp \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"roll_dice","arguments":{"sides":20,"count":3}}}'
    ```

    The `data` line carries the text your handler returned, with your own random rolls:

    ```text theme={null}
    event: message
    data: {"result":{"content":[{"type":"text","text":"Rolled 3d20: [16, 10, 20] total=46"}]},"jsonrpc":"2.0","id":3}
    ```
  </Step>
</Steps>

## Connect the server to Claude Code

With the server answering on its own, register it with Claude Code and have Claude call the tool from a prompt. Run these commands in your second terminal from the `mcp-quickstart` folder, because `claude mcp add` saves the server for the current project folder by default, and Claude Code only sees it when you run from that same folder.

<Steps>
  <Step title="Add the server to Claude Code">
    Register the server under the name `quickstart`, with the HTTP transport and the local URL:

    ```bash theme={null}
    claude mcp add --transport http quickstart http://localhost:3000/mcp
    ```

    Claude Code confirms where it saved the entry:

    ```text theme={null}
    Added HTTP MCP server quickstart with URL: http://localhost:3000/mcp to local config
    ```
  </Step>

  <Step title="Check the connection">
    List your servers. Claude Code connects to each one to check its health:

    ```bash theme={null}
    claude mcp list
    ```

    A working server shows as connected:

    ```text theme={null}
    Checking MCP server health…

    quickstart: http://localhost:3000/mcp (HTTP) - ✔ Connected
    ```

    If it shows an error instead, check that `node server.mjs` is still running in the first terminal.
  </Step>

  <Step title="Ask Claude to roll dice">
    Send Claude one prompt with [`claude -p`](https://code.claude.com/docs/en/headless), which runs a single prompt without opening an interactive session and prints the answer. Claude Code names MCP tools `mcp__<server>__<tool>`, so your tool is `mcp__quickstart__roll_dice`, and `--allowedTools` lets Claude call it without stopping to ask you:

    ```bash theme={null}
    claude -p "Roll three 20-sided dice using the quickstart server and tell me each roll and the total." \
      --allowedTools "mcp__quickstart__roll_dice"
    ```

    Claude calls your server and reports the rolls it got back:

    ```text theme={null}
    Rolled 3d20 via the quickstart server:

    - Roll 1: **12**
    - Roll 2: **4**
    - Roll 3: **5**

    **Total: 21**
    ```
  </Step>

  <Step title="Confirm the tool ran">
    To see the tool call itself rather than Claude's summary of it, run the same prompt with streaming JSON output. `--output-format stream-json` needs `--verbose` alongside it:

    ```bash theme={null}
    claude -p "Roll three 20-sided dice using the quickstart server and tell me each roll and the total." \
      --allowedTools "mcp__quickstart__roll_dice" \
      --output-format stream-json --verbose
    ```

    The output is one JSON object per line, and other tool calls can appear before yours. Look for the `tool_use` block whose `name` is `mcp__quickstart__roll_dice`, carrying the arguments Claude chose, and the `tool_result` block after it carrying your server's text, shown here with the surrounding fields removed:

    ```text theme={null}
    {"type":"tool_use","name":"mcp__quickstart__roll_dice","input":{"count":3,"sides":20}, ...}
    {"type":"tool_result","content":[{"type":"text","text":"Rolled 3d20: [9, 14, 5] total=28"}], ...}
    ```
  </Step>
</Steps>

In an interactive `claude` session you can ask the same thing in your own words. Without `--allowedTools`, Claude Code asks for your permission before it calls `roll_dice`.

## Put the server in front of claude.ai users

Claude Code reached your server because both run on your machine. When someone adds a connector by URL on claude.ai, in the Claude desktop and mobile apps, or in Cowork, Claude connects to it from Anthropic's infrastructure over the internet, so a `localhost` address isn't reachable from there. To use this server on those surfaces, you host it at a public HTTPS URL and people add that URL as a connector.

Once the server has a public URL, these pages cover each step:

* [Add a connector by URL](/docs/connectors/custom/add-unlisted#add-a-connector-by-url): add the server to your own Claude account as a custom connector
* [Test in Claude as a custom connector](/docs/connectors/building/testing#test-in-claude-as-a-custom-connector): check the connection, the tool list, and a real call from a chat, including how to expose a server that's still on your machine through a tunnel
* [Authentication for connectors](/docs/connectors/building/authentication): add sign-in before real users connect, because this quickstart server lets anyone who can reach it call its tools
* [Publish to the directory](/docs/directory/publish): submit the server for review so people can find it in Claude

## Clean up

When you're done, stop the server by pressing `Ctrl+C` in the first terminal. Then, from the `mcp-quickstart` folder, remove the entry from Claude Code:

```bash theme={null}
claude mcp remove quickstart
```

Claude Code confirms with `Removed MCP server "quickstart" from local config`.

## Next steps

* [Add an interactive UI to your MCP server](/docs/connectors/building/mcp-apps/quickstart): give `roll_dice` a small UI that shows the dice inside the conversation
* [Build an MCP server for Claude](/docs/connectors/building/index): plan a real server around what Claude's client supports, including authentication, result size limits, and timeouts
* [Test your connector](/docs/connectors/building/testing): add your hosted server to Claude as a custom connector and debug connection failures
* [Build your first plugin](/docs/plugins/quickstart): bundle your connector with a skill that teaches Claude when to use it, and submit both to the directory

connectors/building/review-criteria Changed · +52 / -30 lines

# Connector pre-submission checklist ## Design tools that pass review ### Avoid prompt-injection patterns ## Server behavior and scope ### Functional quality ### API ownership ### Unsupported use cases ## Gather what to include with your submission ## Test before you submit ## Understand how Anthropic reviews connectors ## Next steps # Pre-submission checklist ## Tool design ## Avoid prompt-injection patterns ## Functional quality ## API ownership ## Unsupported use cases ## Submission requirements ## Before you submit

from line 1
1# Pre-submission checklist
1# Connector pre-submission checklist
22 
3> What Anthropic reviewers test, so you can pass on the first try
3> Check your MCP connector against what Anthropic reviewers test, including tool design, prompt-injection patterns, and functional quality, before you submit.
44 
5When you submit a server, it is automatically scanned for policy compliance and, by default, listed in the directory as a [community connector](/docs/connectors/verification). Anthropic may then escalate listings flagged as highly useful to Claude users to verified review, which is higher touch and slower; reviewers run a functional test of each tool. This escalation is assessed automatically, and you do not need to take any action. Every server in the directory must meet the criteria on this page, whichever label it carries. The label is a quality signal shown to users; it does not change how your connector runs once connected. This page surfaces the most common rejection reasons so you can self-correct before submitting. For the full legal text, see the [Software Directory Policy](https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy).
5This checklist covers what Anthropic checks on an MCP connector submitted to the directory, organized around the most common rejection reasons so you can correct them before you submit. It's for connector authors preparing a [directory submission](/docs/connectors/building/submission). For the full legal text behind these criteria, see the [Software Directory Policy](https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy).
66 
7## Tool design
7<Note>
8 If you're submitting a plugin, see the [plugin pre-submission checklist](/docs/plugins/pre-submission-checklist) instead.
9</Note>
810 
11Work through [tool design](#design-tools-that-pass-review), [server behavior](#server-behavior-and-scope), and [what to include with your submission](#gather-what-to-include-with-your-submission), then [test every tool](#test-before-you-submit) before you open the portal. [How Anthropic reviews connectors](#understand-how-anthropic-reviews-connectors) covers the Community and Verified labels.
12 
13## Design tools that pass review
14 
15Tool design covers how your tools are split, named, annotated, and described. These rules apply to every tool your server exposes.
16 
917### Separate read and write tools
1018 
11A single tool that accepts both safe HTTP methods (GET, HEAD, OPTIONS) and unsafe methods (POST, PUT, PATCH, DELETE) is rejected. Do not ship a catch-all `api_request` tool with a `method` parameter.
19A single tool that accepts both safe HTTP methods, such as GET, HEAD, and OPTIONS, and unsafe methods, such as POST, PUT, PATCH, and DELETE, is rejected. Don't ship a catch-all `api_request` tool with a `method` parameter.
1220 
13Split into a read-only tool and one or more write tools. Ideally, split write operations further by action type (create, update, delete). Documenting safe versus unsafe operations within one tool's description does not satisfy this requirement—the operations must be in separate tools.
21Split a catch-all tool into a read-only tool and one or more write tools. Ideally, split write operations further by action type: create, update, and delete. Documenting safe versus unsafe operations within one tool's description doesn't satisfy this requirement. The operations must be in separate tools.
1422 
1523### Reference API docs in custom query tools
1624 
17If a tool accepts freeform endpoint paths, query strings, or request bodies that the caller constructs, its description must include a link to or explicit name of the target API. For example: "Queries the Slack Web API—see [https://api.slack.com/methods](https://api.slack.com/methods)". A description like "Makes a request to the API" with no further context fails.
25If a tool accepts freeform endpoint paths, query strings, or request bodies that the caller constructs, its description must include a link to or the explicit name of the target API. For example, "Queries the Slack Web API, see [https://api.slack.com/methods](https://api.slack.com/methods)" passes. A description like "Makes a request to the API" with no further context fails.
1826 
19This applies only to custom query tools. Purpose-built tools that call a fixed endpoint internally do not need an API docs reference.
27The API docs requirement applies only to custom query tools. Purpose-built tools that call a fixed endpoint internally don't need an API docs reference.
2028 
2129### Provide tool annotations
2230 
23Every tool must include a `title` and the applicable hint—`readOnlyHint: true` for read-only tools, `destructiveHint: true` for tools that modify or delete data. These determine auto-permissions in Claude: read-only tools can run without per-call confirmation; destructive tools always prompt.
31Every tool must include a `title` and the applicable hint: `readOnlyHint: true` for read-only tools, and `destructiveHint: true` for tools that modify or delete data. These determine auto-permissions in Claude. Read-only tools can run without per-call confirmation, and destructive tools always prompt.
2432 
2533### Keep tool names short
2634 
from line 38
3038 
3139Each tool description should state precisely what the tool does and when to invoke it. The description must match the tool's actual behavior.
3240 
33## Avoid prompt-injection patterns
41### Avoid prompt-injection patterns
3442 
35Tool descriptions are rejected if they:
43Describe what the tool does, and don't tell Claude how to behave. Tool descriptions are rejected if they:
3644 
3745* Instruct Claude to call external software or tools the user didn't request
3846* Interfere with Claude calling other tools
from line 48
4048* Contain hidden, obfuscated, or encoded instructions
4149* Tell Claude to behave in ways unrelated to the tool's function, attempt to override system instructions, or promote products and services
4250 
43Describe what the tool does. Do not tell Claude how to behave.
51## Server behavior and scope
4452 
45## Functional quality
53Beyond individual tools, reviewers check how the server behaves when called, whose APIs it calls, and whether its use case is one the directory accepts.
4654 
47* Every tool must return a successful response when called with valid parameters. Generic errors ("Internal Server Error", "Bad Request" with no detail) fail review.
48* Validate inputs and return actionable error messages rather than silently accepting invalid data.
49* Keep responses reasonably sized for the task. Do not return a full database dump when a summary was requested.
50* Do not collect conversation data beyond what the tool needs for its function.
51* Do not query Claude's memory, chat history, conversation summaries, or user files.
55### Functional quality
5256 
53## API ownership
57* Every tool must return a successful response when called with valid parameters, and generic errors such as "Internal Server Error" or "Bad Request" with no detail fail review
58* Validate inputs and return actionable error messages rather than silently accepting invalid data
59* Keep responses reasonably sized for the task, and don't return a full database dump when a summary was requested
60* Don't collect conversation data beyond what the tool needs for its function
61* Don't query Claude's memory, chat history, conversation summaries, or user files
5462 
63### API ownership
64 
5565Your server must call your own first-party APIs, or APIs you legitimately proxy. The MCP server domain should match your service.
5666 
57## Unsupported use cases
67### Unsupported use cases
5868 
59Connectors that do the following are not accepted:
69Connectors that do the following aren't accepted:
6070 
6171* Transfer money, cryptocurrency, or other financial assets
62* Generate images, video, or audio via AI models (design tools that produce diagrams, charts, or UI mockups are allowed)
72* Generate images, video, or audio through AI models
6373 
64## Submission requirements
74Design tools that produce diagrams, charts, or UI mockups are allowed.
6575 
66* **Test credentials** are required and must be a fully populated account.
67* **Allowed link URIs** are recommended if your server calls `ui/open-link`. Declared HTTPS origins and custom URI schemes open without a confirmation prompt; anything else still prompts the user. See [Allowed link URIs](/docs/connectors/building/submission#allowed-link-uris).
68* **Public documentation** is required by your publish date—a blog post or help-center article is sufficient. You can share docs privately with Anthropic during review.
69* **Plugins** must link a public GitHub repo; closed-source is not accepted.
70* **MCPB** open-source and "spec will evolve" clauses in the [Software Directory Terms](https://support.claude.com/en/articles/13145338-anthropic-software-directory-terms) are required and not waivable.
76## Gather what to include with your submission
7177 
72## Before you submit
78Alongside the server itself, a submission carries credentials, links, and terms acknowledgments that reviewers check:
7379 
74Run `claude plugin validate` on plugins. For MCP servers, exercise every tool through the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) and as a [custom connector in Claude](/docs/connectors/building/testing).
80* **Test credentials**: required, and they must be for a fully populated account
81* **Allowed link URIs**: recommended if your server calls `ui/open-link`. Declared HTTPS origins and custom URI schemes open without a confirmation prompt, and anything else still prompts the user. See [Allowed link URIs](/docs/connectors/building/submission#allowed-link-uris)
82* **Public documentation**: required by your publish date. A blog post or help-center article is sufficient, and you can share docs privately with Anthropic during review
83 
84## Test before you submit
85 
86Exercise every tool through the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) and as a [custom connector in Claude](/docs/connectors/building/testing) before you submit. The portal's **Test & launch** step asks you to confirm you've done this.
87 
88## Understand how Anthropic reviews connectors
89 
90When you submit a server, Anthropic scans it automatically for policy compliance and, by default, lists it in the directory as a [Community connector](/docs/connectors/verification). Anthropic may then escalate listings flagged as highly useful to Claude users to Verified review, which is higher touch and slower, and in which reviewers run a functional test of each tool. That escalation is assessed automatically, and you don't need to take any action. Every server in the directory must meet the criteria on this page, whichever label it carries. The label is a quality signal shown to users and doesn't change how your connector runs once connected.
91 
92## Next steps
93 
94* [Submit a connector](/docs/connectors/building/submission): what the developer portal asks for at each step
95* [Connector verification](/docs/connectors/verification): what the Community and Verified labels mean to the people who install your connector
96* [Manage your directory listing](/docs/connectors/building/managing-your-listing): track review status and reviewer feedback after you submit
7597 

connectors/building/submission Changed · +92 / -78 lines

# Submit a connector to the directory ## Pre-submission checklist for connectors ## Choose where to submit your connector ## Meet the submission requirements ### Directory terms ### Requirements for every connector ### Privacy policy for local connectors ### Allowed link URIs ### Carousel screenshots for MCP Apps ## Submit through the developer portal ## After you submit ## Next steps # Submitting to the Connectors Directory ## What you can submit ## Before you start ## Directory terms & conditions ## Submission requirements ## Privacy policy requirements ## Allowed link URIs ## Asset specifications ### Carousel screenshots (MCP Apps) ## Review process ## Submit your connector ### What to expect in the portal

from line 1
1# Submitting to the Connectors Directory
1# Submit a connector to the directory
22 
3> Submit your MCP connector to the Connectors Directory
3> Submit a remote MCP server or MCP App to the Connectors Directory through the developer portal: requirements, screenshot specs, and what each step asks for.
44 
5The [Connectors Directory](/docs/connectors/directory) aims to be a collection of high-quality, vetted, and reviewed MCP servers that are helpful and harmless to users. Anyone is welcome to build MCP servers, but only servers meeting the review standards outlined on this page will be included in the directory.
5You submit a remote MCP server to the [Connectors Directory](/docs/connectors/directory) through the developer portal at [claude.ai/directory/manage](https://claude.ai/directory/manage), where you choose **MCP connector**.
66 
7## What you can submit
7This page is for connector authors who are ready to submit. It starts with a [checklist](#pre-submission-checklist-for-connectors), then covers [where to submit your connector](#choose-where-to-submit-your-connector), the [requirements every submission must meet](#meet-the-submission-requirements), and [what to have ready for each step of the portal](#submit-through-the-developer-portal). [Connector review criteria](/docs/connectors/building/review-criteria) explains what reviewers look for in your tools and server, so read it alongside the checklist.
88 
9Developers can submit:
9<Note>
10 If you're submitting a plugin rather than a connector, see [Submit a plugin](/docs/plugins/submit) instead.
11</Note>
1012 
11* **Remote MCP servers** — internet-hosted servers that provide tools and data to Claude
12* **Desktop extensions** — local MCP servers packaged as [MCP Bundles (MCPB)](https://github.com/modelcontextprotocol/mcpb) for Claude Desktop
13* **[MCP Apps](/docs/connectors/building/mcp-apps/getting-started)** — MCP servers that surface interactive UI elements. These have the additional requirement of including screenshots for submission and listing in the directory.
13## Pre-submission checklist for connectors
1414 
15## Before you start
15Work through this list before you open the portal. Each item links to where it is explained, and the rest of this page covers the requirements and the portal steps in detail.
1616 
17Remote MCP server submissions happen inside Claude.ai, in the [submission portal](https://claude.ai/admin-settings/directory/submissions/new). The portal is part of your organization's settings, so you need:
17* **Your server is remote and reachable over HTTPS**: the portal asks for an `https://` URL. A local server can't be submitted on its own; include it in a [plugin](/docs/plugins/submit) instead
18* **Authentication works for Claude's client**: OAuth 2.0 if your tools act on a user's account, or no authentication for public data, as [Authentication for connectors](/docs/connectors/building/authentication) describes. You don't need Verified status or any separate approval for authentication to work
19* **Every tool has a `title` and a `readOnlyHint` or `destructiveHint` annotation**: the portal flags tools that are missing them, and [review criteria](/docs/connectors/building/review-criteria#design-tools-that-pass-review) explain how reviewers read tool names and descriptions
20* **You've tested it in Claude**: add the server as a [custom connector](/docs/connectors/building/testing#test-in-claude-as-a-custom-connector) and call each tool from a conversation; the portal's **Test & launch** step asks you to confirm this
21* **You have the listing materials**: documentation URL, privacy policy URL, support contact, an icon, and for an [MCP App](/docs/connectors/building/mcp-apps/getting-started) the [carousel screenshots](#carousel-screenshots-for-mcp-apps)
22* **You have a test account for reviewers**: credentials for a fully populated account, which you enter in the portal and which only reviewers see
23* **Your account can submit**: any paid Claude plan. See [who can submit](/docs/directory/publish#confirm-you-can-submit-to-the-directory)
24* **If a plugin of yours points at this server, submit the server anyway**: the connector listing gives your organization the server's details, authentication configuration, and dashboard, and lets you pair it with the plugin. See [Submit your plugin, and your MCP server as a connector](/docs/directory/publish#submit-your-plugin-and-your-mcp-server-as-a-connector)
1825 
19* **A Team or Enterprise organization.** Organization settings aren't available on individual plans.
20* **Directory management access.** By default, only organization Owners and Primary owners can submit and manage directory listings. On Enterprise, an Owner can delegate this to other members by creating a custom role in **Organization settings > Roles** with either the **Directory** permission (directory submissions only) or the **Libraries** permission (broader: it also covers managing the organization's plugins, connectors, and skills), and assigning that role. Team plans don't have custom roles, so on Team this stays with Owners.
26Then open the [developer portal](https://claude.ai/directory/manage), select **Submit new**, and choose **MCP connector**.
2127 
22Desktop extensions (MCPB) use a separate [submission form](https://clau.de/desktop-extention-submission) and don't require the portal.
28## Choose where to submit your connector
2329 
24## Directory terms & conditions
30A connector submission is an MCP server, and where you submit it depends on whether the server is remote or local:
2531 
32* **Remote MCP servers**: internet-hosted servers that provide tools and data to Claude. Submit them through the [developer portal](https://claude.ai/directory/manage) and choose **MCP connector**
33* **MCP Apps**: remote MCP servers that [surface interactive UI](/docs/connectors/building/mcp-apps/getting-started). Submit them through the portal as remote servers, and include [carousel screenshots](#carousel-screenshots-for-mcp-apps) for the directory listing
34* **Local MCP servers**: desktop extension listings in the directory are deprecated, and the directory no longer accepts local servers packaged as [MCP Bundles (MCPB)](/docs/connectors/building/mcpb). To distribute a local server through the directory, include it in a [plugin](/docs/plugins/submit)
35 
36Skills aren't a standalone submission type. Bundle them in a [plugin](/docs/plugins/submit).
37 
38Anyone on a paid Claude plan can submit through the portal. [Who can submit to the directory](/docs/directory/publish#confirm-you-can-submit-to-the-directory) has the plan, role, and organization requirements, which are the same for connectors and plugins.
39 
40## Meet the submission requirements
41 
42Every connector in the directory must comply with the directory terms and meet a fixed set of security, annotation, authentication, privacy, and documentation requirements. MCP Apps add screenshot requirements, and connectors that open external links can add an allowlist.
43 
44### Directory terms
45 
2646All servers in the directory must comply with:
2747 
2848* [Anthropic Software Directory Terms](https://support.claude.com/en/articles/13145338-anthropic-software-directory-terms)
from line 54
3454* Respond to security issues promptly
3555* Provide accurate descriptions and documentation
3656 
37## Submission requirements
57### Requirements for every connector
3858 
39All MCP connectors submitted to the directory must meet:
59All MCP connectors submitted to the directory must meet these requirements:
4060 
411. **Security**: Meet Anthropic's security standards
422. **Tool annotations**: All tools must include a `title` and the applicable `readOnlyHint` or `destructiveHint`
433. **Authentication**: Use OAuth 2.0 for authenticated services
444. **Privacy Policy**: Local connectors must include privacy policies
455. **Documentation**: Provide clear setup and usage instructions
61* **Security**: meet Anthropic's security standards
62* **Tool annotations**: every tool includes a `title` and the applicable `readOnlyHint` or `destructiveHint`
63* **Authentication**: use OAuth 2.0 for authenticated services
64* **Privacy policy**: local connectors must include privacy policies
65* **Documentation**: provide clear setup and usage instructions
4666 
4767If your connector opens external links, also provide your [allowed link URIs](#allowed-link-uris) so users aren't prompted to confirm each one.
4868 
49## Privacy policy requirements
69### Privacy policy for local connectors
5070 
5171Local connectors must include:
5272 
531. "Privacy Policy" section in README.md
542. `privacy_policies` array in manifest.json (manifest\_version 0.2+)
553. HTTPS URLs to privacy policies
73* A "Privacy Policy" section in `README.md`
74* A `privacy_policies` array in `manifest.json`, for `manifest_version` 0.2 and later
75* HTTPS URLs to privacy policies
5676 
5777The privacy policy must cover:
5878 
from line 86
6686 Missing or incomplete privacy policies result in immediate rejection.
6787</Warning>
6888 
69## Allowed link URIs
89### Allowed link URIs
7090 
71If your connector uses the `ui/open-link` capability to open URLs in the user's browser or native apps, provide the list of link targets your server will request. Claude uses this list to suppress the "Open external link" confirmation prompt for destinations you've declared. Links to any other destination still work—users are simply asked to confirm before the link opens.
91If your connector uses the `ui/open-link` capability to open URLs in the user's browser or native apps, provide the list of link targets your server will request. Claude uses this list to suppress the **Open external link** confirmation prompt for destinations you've declared. Links to any other destination still work, but users are asked to confirm before the link opens.
7292 
73Provide each entry in one of two forms:
93The allowed link URIs list is optional. If you omit it, your connector functions normally, and users see a confirmation prompt each time it opens a link.
7494 
75* **HTTPS origin** — `https://example.com`. Only the scheme and hostname are matched; paths, ports, and query strings are ignored. Subdomains are not implied—list each one (`https://app.example.com`, `https://docs.example.com`).
76* **Custom URI scheme** — `myapp:` for deep links into a native app you own (for example, `spotify:` or `notion:`). Only the scheme is matched.
95Provide each entry in one of these forms:
7796 
78Every origin and scheme you list **must be owned by you** (the submitting organization). You may not list third-party domains or URI schemes registered to apps you don't publish. Entries you don't own will be removed during review.
97* **HTTPS origin**: `https://example.com`. Only the scheme and hostname are matched, and paths, ports, and query strings are ignored. Subdomains aren't implied, so list each one, such as `https://app.example.com` and `https://docs.example.com`
98* **Custom URI scheme**: `myapp:` for deep links into a native app you own. Only the scheme is matched
7999 
80<Note>
81 This field is optional. If omitted, your connector functions normally, but users are shown a confirmation prompt each time it opens a link.
82</Note>
100Every origin and scheme you list must be owned by you, the submitting organization. You may not list third-party domains or URI schemes registered to apps you don't publish. Entries you don't own are removed during review. [Open external links from MCP Apps](/docs/connectors/building/mcp-apps/external-links) explains how Claude matches entries and when the prompt still appears.
83101 
84## Asset specifications
102### Carousel screenshots for MCP Apps
85103 
86### Carousel screenshots (MCP Apps)
104An MCP App submission includes screenshots for its directory listing carousel. Prepare them to these specifications:
87105 
88* **Format:** PNG
89* **Width:** at least 1000px
90* **Count:** 3–5 images
91* **Crop:** to the app response only—**do not include the prompt** in the image
92* **Aspect ratio:** any
93* **Paired prompts:** provide the prompt text separately for each screenshot
94* **Mobile:** no separate mobile assets are required—one batch covers all surfaces
95* **Video/GIF:** not accepted
106* **Format**: PNG
107* **Width**: at least 1000px
108* **Count**: 3–5 images
109* **Crop**: to the app response only, without the prompt in the image
110* **Aspect ratio**: any
111* **Paired prompts**: provide the prompt text separately for each screenshot
112* **Mobile**: no separate mobile assets are required, and one batch covers all surfaces
113* **Video or GIF**: not accepted
96114 
97115A carousel template is available in the [Anthropic MCP Apps Figma community file](https://www.figma.com/community/file/1597641111449594397/mcp-apps-for-claude).
98116 
99117### Detail card description
100118 
101You write the detail card description in the submission portal. It is not editable by Anthropic. The disclaimer text shown on connector cards is general and not customizable per partner.
119You write the detail card description in the submission portal, and Anthropic can't edit it. The disclaimer text shown on connector cards is general and not customizable per partner.
102120 
103## Review process
121## Submit through the developer portal
104122 
105Review times vary with queue volume. The submission portal is always open.
123The developer portal at [claude.ai/directory/manage](https://claude.ai/directory/manage) takes your submission in a series of steps. Before you open it, have these ready:
106124 
107After you submit, track your submission's status and read reviewer feedback in the [submissions dashboard](https://claude.ai/admin-settings/directory/submissions). See [Managing your listing](/docs/connectors/building/managing-your-listing) for what's available there, including server health and usage metrics after publication. Email `[email protected]` for escalations.
125* Your documentation URL and privacy policy URL
126* Your connector's icon
127* Test account credentials for reviewers
128* Carousel screenshots, if you're submitting an MCP App, per the [screenshot specifications](#carousel-screenshots-for-mcp-apps)
108129 
109Run the [pre-submission checklist](/docs/connectors/building/review-criteria) and, for plugins, `claude plugin validate` before you submit.
130Your progress saves automatically in your browser as you move between steps, so within a browser session you can return to earlier steps without losing work. Each step asks for the following:
110131 
111## Submit your connector
112 
113Ready to submit? Use the path that matches your connector type:
114 
115* **Remote MCP servers (including MCP Apps)**: submit through the [submission portal](https://claude.ai/admin-settings/directory/submissions/new) in your organization's settings on Claude.ai. See [Before you start](#before-you-start) for access requirements.
116* **Desktop extensions (MCPB)**: use the [desktop extension submission form](https://clau.de/desktop-extention-submission).
117 
118Skills are not a standalone submission type—bundle them in a [plugin](/docs/plugins/submit).
119 
120### What to expect in the portal
121 
122Before you start, have your documentation URL, privacy policy URL, icon, and test account credentials ready, plus carousel screenshots if you're submitting an MCP App (see [asset specifications](#asset-specifications) above).
123 
124The portal walks you through the following steps. Your progress saves automatically in your browser as you move between steps, so within a browser session you can jump back to earlier steps without losing work.
125 
126132<Steps>
127 <Step title="Introduction">
128 Explains what a directory listing does and doesn't do: inclusion makes your connector discoverable but doesn't change the tools it exposes. The portal accepts remote MCP servers only. Local servers are distributed as [desktop extensions](https://clau.de/desktop-extention-submission) or [plugins](/docs/plugins/submit) instead.
129 </Step>
130 
131133 <Step title="Connection">
132 Connect the server you're submitting. You confirm the server URL (must be `https://`), the transport (streamable HTTP or SSE), and how users reach your server: one **Universal URL** for everyone, a fixed list of **Multiple URLs**, or a **URL pattern** that each user's own URL must match. See [Servers with per-customer URLs](/docs/connectors/building/authentication#servers-with-per-customer-urls) for how this choice limits your authentication options.
134 Connect the server you're submitting, by pasting its `https://` URL or choosing a custom connector you've already added to Claude. If your users connect to different URLs, select **Users connect to different URLs** and choose **Multiple URLs** or **URL pattern**.
133135 </Step>
134136 
135137 <Step title="Tools">
136 Your server's tools, prompts, and resources sync automatically from the connected server, grouped by whether their annotations declare them read-only or write (tools without annotations are grouped separately). If any tools are flagged for missing titles or annotations, fix them on your server before submitting.
138 Your server's tools, prompts, and resources sync automatically from the connected server, grouped by whether their annotations declare them read-only or write. If any tools are flagged for missing titles or annotations, fix them on your server before submitting.
137139 </Step>
138140 
139141 <Step title="Listing">
140 The public-facing listing: server name (100 characters max), tagline (55 characters max), description (2,000 characters max), one to five categories, documentation URL, privacy policy URL, support contact, icon, and the URL slug for your listing page. The slug is permanent once published.
142 The public-facing listing: server name up to 100 characters, one-liner up to 200 characters, description up to 2,000 characters, one to five categories, documentation URL, privacy policy URL, support contact, icon, and the URL slug for your listing page. The slug is permanent once published.
141143 </Step>
142144 
143145 <Step title="Use cases">
144 Describe the primary use cases, what users need before they can connect (accounts, plans, or other setup), and whether the connector reads data, writes data, or both.
146 The primary use cases, what users need before they can connect, such as accounts, plans, or other setup, and whether the connector reads data, writes data, or both.
145147 </Step>
146148 
147149 <Step title="Company">
148 Company name and website, plus a primary contact for review updates. The contact name and email are pre-filled from your account.
150 Company name and website, plus a primary contact for review updates.
149151 </Step>
150152 
151153 <Step title="Authentication">
152 How users authenticate: OAuth (with dynamic client registration, client ID metadata documents, or Anthropic-held client credentials), a custom connection where users supply their own URL or credentials at connection time, or no authentication. See [authentication](/docs/connectors/building/authentication) for which modes are supported out of the box and which need coordination with the review team. If your server starts without authentication and individual tools prompt for it on demand, you can flag that here.
154 How users authenticate: OAuth with dynamic client registration, client ID metadata documents, or Anthropic-held client credentials; a custom connection where users supply their own URL or credentials at connection time; or no authentication. [Authentication](/docs/connectors/building/authentication) covers which modes are supported directly and which need coordination with the review team. If your server starts without authentication and individual tools prompt for it on demand, you can flag that here.
153155 
154156 If you chose **URL pattern** in the Connection step, Anthropic-held client credentials can't be used. If you chose **Multiple URLs**, a custom connection can't be used.
155157 </Step>
from line 161
159161 </Step>
160162 
161163 <Step title="Test & launch">
162 Test-account setup and access instructions detailed enough for a reviewer to access your server end to end: every link, credential, and step, including credentials for a fully populated account where relevant. You also confirm you've run every tool yourself, either via MCP Inspector or as a custom connector in Claude.
164 Test-account setup and access instructions detailed enough for a reviewer to connect to your server and run its tools: every link, credential, and step, including credentials for a fully populated account where relevant. You also confirm you've run every tool yourself, either through MCP Inspector or as a custom connector in Claude.
163165 </Step>
164166 
165167 <Step title="Compliance">
from line 168
166168 Seven policy acknowledgments covering the directory guidelines, first-party API usage, financial transactions, AI media generation, prompt injection, conversation data collection, and public documentation. All seven are required.
167169 </Step>
168170 
169 <Step title="Review">
170 A final read-through of everything you've entered. Any quality warnings (for example, very short answers) are shown here and shared with the review team alongside your submission. Submit when you're ready.
171 <Step title="Review and submit">
172 Everything you've entered, for a final check before you submit. Any quality warnings, such as very short answers, appear here and are shared with the review team alongside your submission.
171173 </Step>
172174</Steps>
173175 
174After you submit, your submission's status and any reviewer feedback appear in the [submissions dashboard](https://claude.ai/admin-settings/directory/submissions). See [Managing your listing](/docs/connectors/building/managing-your-listing).
176## After you submit
177 
178Anthropic scans your submission automatically for policy compliance and, by default, lists it as a Community connector with no action from you. Some submissions also get a review from a person, and those review times vary with queue volume. The portal is always open for new submissions. Your submission's status and any reviewer feedback appear in the portal at [claude.ai/directory/manage](https://claude.ai/directory/manage), and [Track your directory submission](/docs/directory/submission-status#mcp-connector-statuses) explains what each status in the portal means. [Manage your directory listing](/docs/connectors/building/managing-your-listing) covers reviewer feedback, listing edits, and the server health and usage metrics you get after publication.
179 
180For escalations, email `[email protected]`.
181 
182If a submission is stuck, [Contact Anthropic about a submission](/docs/directory/submission-status#contact-anthropic-about-a-submission) gives the channel for an MCP connector.
183 
184## Next steps
185 
186* [Manage your directory listing](/docs/connectors/building/managing-your-listing): track review status, respond to reviewer feedback, and edit your listing
187* [After publishing](/docs/connectors/building/after-publishing): release updates to your server, and delist
188* [Connector verification](/docs/connectors/verification#list-your-own-connector): see how a Community listing becomes Verified and what each label means to users
175189 

connectors/building/testing Changed · +46 / -16 lines

# Test your connector ## Test in Claude as a custom connector ### Test a local server ### Validate with MCP Inspector ## Debug connection failures ## Next steps # Testing your connector ## Test as a custom connector ## Test a local server ## Validate with MCP Inspector ## Debugging

from line 1
1# Testing your connector
1# Test your connector
22 
3> Test your MCP server against Claude before submitting to the directory
3> Test your MCP server against Claude as a custom connector before you submit it to the directory, including a local server exposed through a tunnel.
44 
5Test your server against the real Claude client before submitting. There is no separate staging environment—you test in production using a custom connector.
5You test an MCP server against the real Claude client by adding it to Claude as a custom connector. There is no separate staging environment, so you test in production. Custom connectors use the exact same runtime as directory connectors, and what works as a custom connector will work after publication.
66 
7## Test as a custom connector
7This page is for developers with a running server, whether it's deployed or still on your machine. It covers adding the server to Claude, validating it with MCP Inspector, detecting Claude as the client, and preparing test credentials for directory review. If a connection fails while you test, [Troubleshoot your connector](/docs/connectors/building/troubleshooting) walks through each error message.
88 
9Any Claude account (Free, Pro, Max, Team, or Enterprise) can add a custom connector. Go to **Customize > Connectors**, select **Add custom connector**, and enter your server's URL. Custom connectors use the exact same runtime as directory connectors, so what works here will work after publication.
9## Test in Claude as a custom connector
1010 
11## Test a local server
11To test your server the way users will reach it, add it to your own Claude account as a custom connector by its URL. Any Claude account can do this, on Free, Pro, Max, Team, or Enterprise; [Add a connector that isn't in the directory](/docs/connectors/custom/add-unlisted#add-a-connector-by-url) has the steps for each plan, starting from [**Customize > Connectors**](https://claude.ai/customize/connectors).
1212 
13To test a server running on your machine, expose it as a public URL with a tunnel such as [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) or `ngrok`, then add the tunnel URL as a custom connector. This is the recommended pattern for iterating on MCP Apps as well.
13Once the connector is added, check these:
1414 
15* **The connection**: the connector shows **Connected** under **Your connectors**. If it shows **Connect** or **Reconnect** instead, sign-in didn't finish, and [Debug connection failures](#debug-connection-failures) covers the usual causes
16* **Your tools**: open the connector's page and look under **Tool permissions**. Every tool your server advertises should be listed with the name and description you gave it, because those are what Claude reads when it decides to call a tool
17* **A real call**: in a chat, turn the connector on from **+ > Connectors**, ask for something that needs one of your tools, and approve the call when Claude asks. Confirm on your server that the request arrived with the arguments you expected, and in Claude that the result reads well. [Use the connector in a conversation](/docs/connectors/getting-started#use-the-connector-in-a-conversation) shows what the user sees at each point
18 
19### Test a local server
20 
21Claude reaches your server from Anthropic's infrastructure, so a server running on your machine needs a public URL. Expose it with a tunnel such as [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) or `ngrok`, then add the tunnel URL as a custom connector. This is the recommended pattern for iterating on MCP Apps as well.
22 
23If your server uses the TypeScript SDK's `createMcpExpressApp()`, as the [quickstart server](/docs/connectors/building/quickstart) does, its DNS rebinding protection accepts only `localhost`, `127.0.0.1`, and `[::1]` in the `Host` header. A request that arrives with the tunnel's hostname in that header gets `403` with `Invalid Host: <hostname>`, and the connection fails. While you test through the tunnel, pass the tunnel's hostname in `allowedHosts`, replacing `abc123.example.com` with your tunnel's hostname:
24 
25```js theme={null}
26const app = createMcpExpressApp({ allowedHosts: ['localhost', '127.0.0.1', 'abc123.example.com'] });
27```
28 
1529<Warning>
1630 A tunnel exposes your local server to the public internet. Keep authentication enabled on your server while tunneling, and shut the tunnel down when you're done testing.
1731</Warning>
1832 
19## Validate with MCP Inspector
33### Validate with MCP Inspector
2034 
21Use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to verify protocol compliance, exercise your auth flow, and inspect tool schemas before connecting to Claude.
35Before connecting to Claude, use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to verify protocol compliance, exercise your auth flow, and inspect tool schemas. With your server running, run the Inspector's command-line mode from a terminal against your server's URL, replacing `http://localhost:3000/mcp` with your own:
2236 
37```bash theme={null}
38npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp --transport http --method tools/list
39```
40 
41The command lists the tools your server advertises. Change `--method` to `tools/call` with `--tool-name` and `--tool-arg` options to call one.
42 
2343## Detect Claude as the client
2444 
25Claude identifies itself in the MCP `initialize` handshake via `clientInfo`, but the exact value depends on the surface and the request path. You may see `"name": "claude-ai"`, `"name": "Anthropic"` (sometimes with a service suffix), or `"name": "claude-code"`:
45Claude identifies itself in the MCP `initialize` handshake through `clientInfo`, but the exact value depends on the surface and the request path. You may see `"name": "claude-ai"`, `"name": "Anthropic"`, sometimes with a service suffix, or `"name": "claude-code"`. One handshake looks like this:
2646 
2747```json theme={null}
2848{ "clientInfo": { "name": "Anthropic", "version": "1.0.0" } }
2949```
3050 
31Don't gate behavior on an exact `name` or `version` string — both vary across surfaces, request paths, and releases. Use `clientInfo` for telemetry and coarse feature detection only, and remember it's unauthenticated: any client can claim any name, so it must never feed an authorization decision.
51Don't gate behavior on an exact `name` or `version` string, because both vary across surfaces, request paths, and releases. Use `clientInfo` for telemetry and coarse feature detection only. It's also unauthenticated: any client can claim any name, so it must never feed an authorization decision.
3252 
3353## Prepare test credentials for review
3454 
35Directory submission requires test credentials. Provide a **fully populated account**—not an empty shell—so reviewers can exercise real functionality (list real records, search real data, exercise write tools on real resources). Include step-by-step setup instructions for someone unfamiliar with your service.
55Directory submission requires test credentials. Provide a fully populated account rather than an empty shell, so reviewers can exercise real functionality: list real records, search real data, and exercise write tools on real resources. Include step-by-step setup instructions for someone unfamiliar with your service.
3656 
37## Debugging
57## Debug connection failures
3858 
39Partner-visible error logs are in development. In the meantime, use server-side logging on your end and the MCP Inspector to diagnose connection failures. Common causes of `initialize` timeouts include slow OAuth endpoints (keep discovery, registration, and token responses under ten seconds; see [endpoint latency](/docs/connectors/building/authentication#endpoint-latency)), overly strict `Origin`-header validation rejecting Anthropic's requests, and firewalls dropping Anthropic's egress traffic.
59Use server-side logging on your end and the MCP Inspector to diagnose connection failures. An `initialize` timeout commonly has one of these causes:
4060 
41If your infrastructure logs show `403 Forbidden` responses your application didn't generate, your CDN or WAF is likely blocking Anthropic's traffic. See [firewall or WAF blocks Anthropic's traffic](/docs/connectors/building/troubleshooting#2-firewall-or-waf-blocks-anthropic%E2%80%99s-traffic) for the fix.
61* **Slow OAuth endpoints**: keep discovery, registration, and token responses under ten seconds, as described in [endpoint latency](/docs/connectors/building/authentication#endpoint-latency)
62* **Strict `Origin`-header validation**: an overly strict check rejects Anthropic's requests
63* **Firewalls**: a firewall drops Anthropic's egress traffic
4264 
43For a structured walkthrough of "Couldn't reach the MCP server" and "Authorization failed" errors, including DNS resolution checks, OAuth discovery diagnostics, and how to find the `ofid_` reference ID to include in a support request, see [troubleshooting connectors](/docs/connectors/building/troubleshooting).
65If your infrastructure logs show `403 Forbidden` responses your application didn't generate, your CDN or WAF is likely blocking Anthropic's traffic. See [firewall or WAF blocks traffic from Anthropic](/docs/connectors/building/troubleshooting#firewall-or-waf-blocks-traffic-from-anthropic) for the fix.
66 
67For a structured walkthrough of the "Couldn't reach the MCP server" and "Authorization failed" errors, see [Troubleshoot your connector](/docs/connectors/building/troubleshooting). It includes DNS resolution checks, OAuth discovery diagnostics, and how to find the `ofid_` reference ID to include in a support request.
68 
69## Next steps
70 
71* [Troubleshoot your connector](/docs/connectors/building/troubleshooting): diagnose each error message Claude shows for a failed connection or tool call
72* [Plugin structure and testing](/docs/plugins/build): bundle your tested connector with skills so people install both together
73* [Publish to the directory](/docs/directory/publish): submit your connector for review and listing
4474 

connectors/building/troubleshooting Changed · +93 / -86 lines

# Troubleshoot your connector ## Couldn't reach the MCP server ### Hostname resolves to a private IP ### Firewall or WAF blocks traffic from Anthropic ### Your server URL redirects to a different host ### OAuth discovery fails ## Authorization with the MCP server failed ## Unexpected error while invoking tool ## Report the problem to Anthropic ## Related resources # Troubleshooting connectors ## Find your reference ID ## "Couldn't reach the MCP server" ### 1. Hostname resolves to a private IP ### 2. Firewall or WAF blocks Anthropic's traffic ### 3. Your server URL redirects to a different host ### 4. OAuth discovery fails ## "Authorization with the MCP server failed" ## "Unexpected error while invoking tool" ## Related topics

from line 1
1# Troubleshooting connectors
1# Troubleshoot your connector
22 
33> Diagnose and resolve common connection, authorization, and tool-call failures for custom and directory MCP connectors
44 
5This page covers the most common reasons a connector fails to connect, authenticate, or run a tool, and how to diagnose each one. Each error Claude shows covers more than one root cause, so start with the section for the message you see:
5These errors appear in Claude, on claude.ai or in the desktop app, when someone uses your MCP server as a connector: either when they select **Connect** and sign in, or later when Claude calls one of your tools in a conversation. You usually meet them first yourself while [testing your server as a custom connector](/docs/connectors/building/testing#test-in-claude-as-a-custom-connector) before you submit it, and the same messages are what your users see after it's listed. Each message covers several causes on your side.
66 
7* "Couldn't reach the MCP server", when Claude can't complete the connection handshake
8* "Authorization with the MCP server failed", when the OAuth flow starts but doesn't complete, or when your server URL redirects to a different host
9* "Unexpected error while invoking tool", when the connector is connected but a tool call fails
7This page is for the developer of the MCP server. Start with the section for the message Claude showed:
108 
11## Find your reference ID
9* [Couldn't reach the MCP server](#couldn%E2%80%99t-reach-the-mcp-server): Claude can't complete the connection handshake with your server
10* [Authorization with the MCP server failed](#authorization-with-the-mcp-server-failed): the OAuth flow starts but doesn't complete, or your server URL redirects to a different host
11* [Unexpected error while invoking tool](#unexpected-error-while-invoking-tool): the connector is connected but a tool call fails
1212 
13When a connection fails, the error toast and the page URL include a reference ID that starts with `ofid_`. For example:
13If none of the causes match, run the [diagnostic checklist](#diagnostic-checklist), then [report the problem to Anthropic](#report-the-problem-to-anthropic) with the reference ID from the error.
1414 
15```text theme={null}
16.../customize/connectors?step=start_error&flow_id=ofid_d32594c73257a651
17```
15## Couldn't reach the MCP server
1816 
19Copy that ID and include it in any GitHub issue or support request. It lets Anthropic trace the exact failure on the server side. Reference IDs are time-limited, so report them soon after the failure.
17This error appears when Claude can't complete the connection handshake. Despite the wording, it isn't always a network failure. Work through these causes in the order listed.
2018 
21<Tip>
22 If you're filing on the [`anthropics/claude-ai-mcp` issue tracker](https://github.com/anthropics/claude-ai-mcp/issues), include the `ofid_` value, your server URL, and what your server-side access logs show during the Connect attempt.
23</Tip>
19### Hostname resolves to a private IP
2420 
25## "Couldn't reach the MCP server"
21claude.ai connectors run on Anthropic's infrastructure and reach your server over the public internet. Before making any request, Claude resolves your server's hostname and validates the result. If any resolved address isn't globally routable, Claude rejects the connection before any HTTP request leaves Anthropic's network. Your server's access logs see nothing, and Claude reports "Couldn't reach."
2622 
27This error appears when Claude can't complete the connection handshake. Despite the wording, it isn't always a network failure. Work through these causes in order.
23Claude rejects the connection when the hostname meets any of these conditions:
2824 
29### 1. Hostname resolves to a private IP
25* Resolves to a private address in `10.0.0.0/8`, `172.16.0.0/12`, or `192.168.0.0/16`
26* Resolves to a carrier-grade NAT address in `100.64.0.0/10`
27* Resolves to a loopback or link-local address
28* Resolves to a mix of public and non-public addresses, because every returned address must be globally routable
29* Has no `A` record from public DNS. Connectors are IPv4-only, so a hostname that only publishes `AAAA` records can't be reached
3030 
31claude.ai connectors run on Anthropic's infrastructure and reach your server over the public internet. Before making any request, Claude resolves your server's hostname and validates the result. If **any** resolved address is not globally routable, Claude rejects the connection before any HTTP request leaves Anthropic's network. Your server's access logs see nothing, and Claude reports "Couldn't reach."
31These setups commonly produce a non-routable address:
3232 
33Claude rejects the connection when the hostname:
33* **Works in Claude Code or `curl` but not claude.ai**: the CLI and `curl` connect from your machine, while claude.ai connects from Anthropic's servers. If your hostname resolves differently inside and outside your network, known as split-horizon DNS, claude.ai may be getting a private IP
34* **Dynamic DNS providers**: dynamic DNS hostnames often resolve to a home network behind NAT or carrier-grade NAT
35* **Internal corporate DNS**: a hostname that resolves on your VPN won't resolve to a routable address from the public internet
3436 
35* resolves to a private address (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)
36* resolves to a carrier-grade NAT address (`100.64.0.0/10`)
37* resolves to a loopback or link-local address
38* resolves to a mix of public and non-public addresses — every returned address must be globally routable
39* has no `A` record from public DNS — connectors are IPv4-only, so a hostname that only publishes `AAAA` records can't be reached
40 
41**Common gotchas:**
42 
43* **Works in Claude Code or `curl` but not claude.ai.** The CLI and `curl` connect from your machine, while claude.ai connects from Anthropic's servers. If your hostname resolves differently inside and outside your network (split-horizon DNS), claude.ai may be getting a private IP.
44* **Dynamic DNS providers.** Dynamic DNS hostnames often resolve to a home network behind NAT or carrier-grade NAT.
45* **Internal corporate DNS.** A hostname that resolves on your VPN won't resolve to a routable address from the public internet.
46 
4737**How to check:** Run `dig +short your-server.example.com` from a machine outside your network, or use a public DNS lookup service. Every returned address must be globally routable.
4838 
49**How to fix:** Expose your server through a publicly-routable endpoint, such as a cloud host with a public IP, a public reverse proxy, or a tunnel. See [test a local server](/docs/connectors/building/testing#test-a-local-server) for the recommended tunnel setup.
39**How to fix:** Expose your server through a publicly routable endpoint, such as a cloud host with a public IP, a public reverse proxy, or a tunnel. See [test a local server](/docs/connectors/building/testing#test-a-local-server) for the recommended tunnel setup.
5040 
51### 2. Firewall or WAF blocks Anthropic's traffic
41### Firewall or WAF blocks traffic from Anthropic
5242 
5343If your hostname resolves correctly but a CDN, WAF, bot-management rule, or rate limiter in front of your server blocks the request, the connection fails before your application sees it.
5444 
from line 46
5646 
5747**How to fix:** Allowlist Anthropic's published outbound IP range in your WAF or CDN configuration, or exempt your MCP and OAuth paths from the blocking rule. The current range is on the [IP address reference](https://platform.claude.com/docs/en/api/ip-addresses) page.
5848 
59### 3. Your server URL redirects to a different host
49### Your server URL redirects to a different host
6050 
61If your registered MCP URL returns a `301`/`302`/`307`/`308` redirect to a different host (apex to `www.`, region routing, vanity domain to CDN), the `Authorization` header is dropped on the redirect per standard HTTP client security behavior. The redirect target receives an unauthenticated request and returns `401`, and the connection fails with "Authorization with the MCP server failed."
51If your registered MCP URL returns a `301`, `302`, `307`, or `308` redirect to a different host, such as apex to `www.`, region routing, or vanity domain to CDN, Claude drops the `Authorization` header on the redirect per standard HTTP client security behavior. The redirect target receives an unauthenticated request and returns `401`, and the connection fails with "Authorization with the MCP server failed."
6252 
63This also explains the common report "works in MCP Inspector or Claude Code CLI but not claude.ai." Local clients fail fast on a redirect, so the misconfiguration is visible immediately. claude.ai follows the redirect, drops the credential, and the failure surfaces later as an authorization error.
53The dropped `Authorization` header on a redirect also explains the common report "works in MCP Inspector or Claude Code CLI but not claude.ai." Local clients fail fast on a redirect, so the misconfiguration is visible immediately. claude.ai follows the redirect, drops the credential, and the failure surfaces later as an authorization error.
6454 
6555**How to check:** Run `curl -sI https://your-server.example.com/your-mcp-path` and look at the response status and `Location` header. If you see a `3xx` status pointing at a different host, that target is the URL you should register.
6656 
6757**How to fix:** Register the URL your server actually listens on, not a URL that redirects to it. Common culprits are apex-to-`www.` canonicalization, geographic or region routing, and vanity-domain-to-CDN redirects.
6858 
69### 4. OAuth discovery fails
59### OAuth discovery fails
7060 
71If your server requires authentication, Claude performs OAuth discovery before it can connect. A discovery failure can surface as "Couldn't reach" even though your MCP endpoint itself is reachable, or as a sign-in that redirects to `/authorize` on your MCP server's host and fails there. The redirect happens when Claude can't read your [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) protected resource metadata and falls back to treating your MCP server's origin as the authorization server, so the browser opens a sign-in page that doesn't exist on your server. The most common causes:
61If your server requires authentication, Claude performs OAuth discovery before it can connect. A discovery failure can surface as "Couldn't reach" even though your MCP endpoint itself is reachable, or as a sign-in that redirects to `/authorize` on your MCP server's host and fails there. The redirect happens when Claude can't read your [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) protected resource metadata and falls back to treating your MCP server's origin as the authorization server, so the browser opens a sign-in page that doesn't exist on your server. These are the most common causes:
7262 
73* **Discovery metadata returns 404.** If your `401` response doesn't include a `WWW-Authenticate` header with a `resource_metadata` pointer, Claude looks for protected resource metadata and authorization server metadata at the standard `/.well-known/` paths on your MCP server's origin. If those paths return `404` and you haven't pointed Claude elsewhere, Claude can't locate your authorization server.
74* **No way to register a client.** Claude needs one of: [RFC 7591 dynamic client registration](https://www.rfc-editor.org/rfc/rfc7591) (a `registration_endpoint` in your authorization server metadata), [Client ID Metadata Documents](/docs/connectors/building/authentication#dcr-and-cimd-details) (`"client_id_metadata_document_supported": true`), or a pre-registered client. Without any of these, Claude can't obtain a client identity. See [supported authentication types](/docs/connectors/building/authentication#supported-authentication-types).
75* **Authorization server is on a different host than the MCP server.** Claude discovers protected resource metadata from your MCP server, then makes a *second* round of discovery requests against the authorization server host listed in `authorization_servers`. If that host lives behind a different CDN or WAF, it must also be reachable from Anthropic's egress range. See [cross-host authorization servers](/docs/connectors/building/authentication#cross-host-authorization-servers).
76* **A proxy or hosting platform alters the discovery response.** A layer in front of your server can rename or drop the `WWW-Authenticate` header, or answer `403` on the `/.well-known/` paths before the request reaches your application. Check the response at the deployed edge with `curl` from a public network, not with a tool that runs inside your platform.
63* **Discovery metadata returns 404**: if your `401` response doesn't include a `WWW-Authenticate` header with a `resource_metadata` pointer, Claude looks for protected resource metadata and authorization server metadata at the standard `/.well-known/` paths on your MCP server's origin. If those paths return `404` and you haven't pointed Claude elsewhere, Claude can't locate your authorization server
64* **No way to register a client**: Claude needs one of [RFC 7591 dynamic client registration](https://www.rfc-editor.org/rfc/rfc7591) (a `registration_endpoint` in your authorization server metadata), [Client ID Metadata Documents](/docs/connectors/building/authentication#dcr-and-cimd-details) (`"client_id_metadata_document_supported": true`), or a pre-registered client. Without any of these, Claude can't obtain a client identity. See [supported authentication types](/docs/connectors/building/authentication#supported-authentication-types)
65* **Authorization server is on a different host than the MCP server**: Claude discovers protected resource metadata from your MCP server, then makes a second round of discovery requests against the authorization server host listed in `authorization_servers`. If that host is behind a different CDN or WAF, it must also be reachable from Anthropic's egress range. See [Serve discovery metadata](/docs/connectors/building/authentication#serve-discovery-metadata)
66* **A proxy or hosting platform alters the discovery response**: a layer in front of your server can rename or drop the `WWW-Authenticate` header, or answer `403` on the `/.well-known/` paths before the request reaches your application. Check the response at the deployed edge with `curl` from a public network, not with a tool that runs inside your platform
7767 
78**How to check:** From a public network, run:
68**How to check:** From a public network, request each discovery document:
7969 
8070```bash theme={null}
8171curl -i https://your-server.example.com/.well-known/oauth-protected-resource
from line 73
8373curl -i https://your-server.example.com/.well-known/openid-configuration
8474```
8575 
86If your MCP endpoint includes a path component (such as `https://your-server.example.com/mcp`), append it to the well-known path: `/.well-known/oauth-protected-resource/mcp`.
76If your MCP endpoint includes a path component, such as `https://your-server.example.com/mcp`, append it to the well-known path: `/.well-known/oauth-protected-resource/mcp`.
8777 
88The protected resource metadata should return `200` with valid JSON. For authorization server metadata, your server only needs to answer **one** of the two discovery endpoints — Claude tries `/.well-known/oauth-authorization-server` ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) first, then falls back to `/.well-known/openid-configuration` ([OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)). A `404` on one is expected if the other returns `200`. Most hosted identity providers (Auth0, Okta, Microsoft Entra, Keycloak, Supabase Auth) only serve `/.well-known/openid-configuration`.
78The protected resource metadata should return `200` with valid JSON. For authorization server metadata, your server only needs to answer one of the two discovery endpoints. Claude tries `/.well-known/oauth-authorization-server` ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) first, then falls back to `/.well-known/openid-configuration` ([OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)). A `404` on one is expected if the other returns `200`. Most hosted identity providers, including Auth0, Okta, Microsoft Entra, Keycloak, and Supabase Auth, only serve `/.well-known/openid-configuration`.
8979 
9080Whichever metadata document resolves should advertise a `registration_endpoint` (DCR), `"client_id_metadata_document_supported": true` (CIMD), or you should be using pre-registered credentials. In a cross-host setup, run the protected-resource curl against your MCP server and the two authorization-server curls against your authorization server's issuer host.
9181 
92## "Authorization with the MCP server failed"
82## Authorization with the MCP server failed
9383 
94This error usually appears after the OAuth flow has started. The most common causes:
84This error usually appears after the OAuth flow has started. These are the most common causes:
9585 
96* **Issuer mismatch.** The `issuer` value in your authorization server metadata must match the issuer that signs your tokens. If your tokens come from a third-party identity provider such as Supabase Auth or Auth0 but your metadata advertises a different issuer URL, validation can fail.
97* **Audience mismatch.** The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-handling) requires your server to verify each access token was issued for it. Claude sends the [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) `resource` parameter on authorization and token requests, set to the canonical form of your MCP server URL — lowercase scheme and host, no trailing slash, no fragment, no default port — including any path component. Your authorization server should issue tokens with that audience, and your MCP server should accept the canonical value when checking `aud` rather than doing a strict byte-for-byte comparison against what the user typed. Or use whatever audience-binding mechanism your token format supports, as long as it confirms the token was minted for your server and not another service.
98* **PKCE not supported.** Claude includes a PKCE `code_challenge` with `code_challenge_method=S256` in every authorization request. If your authorization server doesn't implement S256 PKCE, the flow fails at the token endpoint. The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-code-protection) also requires authorization servers to advertise `"code_challenge_methods_supported": ["S256"]` so spec-compliant clients can verify support before starting the flow.
99* **Refresh failures.** Use RFC 6749-compliant error codes when a refresh token expires. See [token refresh](/docs/connectors/building/authentication#token-refresh).
100* **Slow token endpoint.** Claude waits up to 10 seconds for your `/token` response; if no response bytes arrive in that window, the flow fails here even if your server eventually issues the token. Check the end-to-end latency of your token handler and any proxy or gateway in front of it. See [endpoint latency](/docs/connectors/building/authentication#endpoint-latency).
101* **Your server URL redirects to a different host.** When the URL you registered redirects to another hostname, Claude drops the `Authorization` header as it follows the redirect, the redirect target returns `401`, and the connection fails with this message. See "3. Your server URL redirects to a different host" under "Couldn't reach the MCP server" on this page for how to find and fix the redirect.
86* **Issuer mismatch**: the `issuer` value in your authorization server metadata must match the issuer that signs your tokens. If your tokens come from a third-party identity provider but your metadata advertises a different issuer URL, validation can fail
87* **Audience mismatch**: the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#token-handling) requires your server to verify each access token was issued for it. Claude sends the [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) `resource` parameter on authorization and token requests, set to the canonical form of your MCP server URL, including any path component. The canonical form has a lowercase scheme and host, no trailing slash, no fragment, and no default port. Your authorization server should issue tokens with that audience, and your MCP server should accept the canonical value when checking `aud` rather than doing a strict byte-for-byte comparison against what the user typed. Or use whatever audience-binding mechanism your token format supports, as long as it confirms the token was minted for your server and not another service
88* **PKCE not supported**: Claude includes a PKCE `code_challenge` with `code_challenge_method=S256` in every authorization request. If your authorization server doesn't implement S256 PKCE, the flow fails at the token endpoint. The [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-code-protection) also requires authorization servers to advertise `"code_challenge_methods_supported": ["S256"]` so spec-compliant clients can verify support before starting the flow
89* **Refresh failures**: use RFC 6749-compliant error codes when a refresh token expires. See [token refresh](/docs/connectors/building/authentication#token-refresh)
90* **Slow token endpoint**: Claude waits up to 10 seconds for your `/token` response. If no response bytes arrive in that window, the flow fails here even if your server eventually issues the token. Check the end-to-end latency of your token handler and any proxy or gateway in front of it. See [endpoint latency](/docs/connectors/building/authentication#endpoint-latency)
91* **Your server URL redirects to a different host**: when the URL you registered redirects to another hostname, Claude drops the `Authorization` header as it follows the redirect, the redirect target returns `401`, and the connection fails with this message. See [Your server URL redirects to a different host](#your-server-url-redirects-to-a-different-host) for how to find and fix the redirect
10292 
10393### Microsoft Entra ID rejects the resource value
10494 
105If your authorization server is Microsoft Entra ID and the token request fails with `AADSTS9010010` (sometimes surfaced as `invalid_target`), Entra is rejecting the `resource` value Claude sends because it does not match any Application ID URI registered on your app. Claude sets `resource` to your MCP server URL, including the path, and Entra issues a token when that value is listed under **Expose an API** → **Application ID URI** (`identifierUris` in the manifest) on the app registration that represents your protected API. The default `api://{client-id}` URI alone is not sufficient here, because Claude sends the full MCP server URL as the resource value.
95If your authorization server is Microsoft Entra ID and the token request fails with `AADSTS9010010`, sometimes surfaced as `invalid_target`, Entra is rejecting the `resource` value Claude sends because it doesn't match any Application ID URI registered on your app. Claude sets `resource` to your MCP server URL, including the path. Entra issues a token when that value is listed under **Expose an API > Application ID URI**, which is `identifierUris` in the manifest, on the app registration that represents your protected API. The default `api://{client-id}` URI alone isn't sufficient here, because Claude sends the full MCP server URL as the resource value.
10696 
107**How to fix:**
97**How to fix:** Register the MCP server URL on the API app registration:
10898 
1091. In the [Microsoft Entra admin center](https://entra.microsoft.com), open the app registration that represents your protected API. If you have separate registrations for the OAuth client and the API, this is the API registration, not the client.
1102. Under **Expose an API**, add your MCP server URL as an additional [Application ID URI](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis). The value must match exactly, including the path, without a trailing slash.
1113. If your server validates the token audience (for example, through Azure App Service Authentication), add the API app's Application (client) ID and its `api://` URI to the allowed token audiences so your server accepts tokens issued for the API. This is the **Allowed token audiences** setting, not **Allowed client applications**, which is a different list.
1124. If the OAuth client and the API are separate app registrations, confirm the client has an admin-consented API permission for the scope your API exposes.
99<Steps>
100 <Step title="Open the API app registration">
101 In the [Microsoft Entra admin center](https://entra.microsoft.com), open the app registration that represents your protected API. If you have separate registrations for the OAuth client and the API, this is the API registration, not the client.
102 </Step>
113103 
104 <Step title="Add the MCP server URL as an Application ID URI">
105 Under **Expose an API**, add your MCP server URL as an additional [Application ID URI](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis). The value must match exactly, including the path, without a trailing slash.
106 </Step>
107 
108 <Step title="Allow the API's token audiences">
109 If your server validates the token audience, such as through Azure App Service Authentication, add the API app's Application (client) ID and its `api://` URI to the allowed token audiences so your server accepts tokens issued for the API. This is the **Allowed token audiences** setting, not **Allowed client applications**, which is a different list.
110 </Step>
111 
112 <Step title="Confirm the client's API permission">
113 If the OAuth client and the API are separate app registrations, confirm the client has an admin-consented API permission for the scope your API exposes.
114 </Step>
115</Steps>
116 
114117<Note>
115118 By default, Microsoft Entra accepts a new Application ID URI only if it contains your tenant ID, your app ID, or a domain verified in your tenant, as described in [Microsoft's identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions). If your MCP server runs on a platform hostname, such as `*.azurewebsites.net`, Entra rejects that URL when you add it under **Expose an API**, so serve the server from a custom domain that your tenant has verified and register that URL instead. A tenant administrator can also exempt your app registration from this policy so that Entra accepts a noncompliant URI. Microsoft notes that an `https://` URI can require a verified domain even then, which makes the custom domain the dependable fix.
116119</Note>
117120 
118If the OAuth flow completes successfully on your server (you see the token issued in your logs) but the connection still fails, file a [GitHub issue](https://github.com/anthropics/claude-ai-mcp/issues) with the `ofid_` reference ID and the timestamps from your server's OAuth logs.
121If the OAuth flow completes successfully on your server, meaning you see the token issued in your logs, but the connection still fails, file a [GitHub issue](https://github.com/anthropics/claude-ai-mcp/issues) with the `ofid_` reference ID and the timestamps from your server's OAuth logs.
119122 
120## "Unexpected error while invoking tool"
123## Unexpected error while invoking tool
121124 
122125This error, followed by the name of the tool, appears when your connector shows as connected and signed in but one of its tool calls fails. Claude's tool call reached your server, and your server returned an error result for it. A failed tool call isn't a connection failure, so there is no `ofid_` reference ID for it.
123126 
124**How to check:**
127**How to check:** Compare the failing call inside and outside Claude:
125128 
1261. Run the same tool call against your server in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) and compare the result with the error Claude reports.
1272. Check your server's logs for the tool handler's error at the time of the failure, and whether the failure affects every user of your connector or one account.
129<Steps>
130 <Step title="Run the call in MCP Inspector">
131 Run the same tool call against your server in [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) and compare the result with the error Claude reports.
132 </Step>
128133 
129If you file a [GitHub issue](https://github.com/anthropics/claude-ai-mcp/issues) about a tool-call failure, include the tool name, the time of the failure in UTC, and the connector URL in place of an `ofid_` reference ID.
134 <Step title="Check your server's logs">
135 Check your server's logs for the tool handler's error at the time of the failure, and whether the failure affects every user of your connector or one account.
136 </Step>
137</Steps>
130138 
139If none of these is the cause, [report the problem](#report-the-problem-to-anthropic) with the tool name and the time of the failure.
140 
131141## Diagnostic checklist
132142 
133Run through these in order before filing an issue:
143The checklist covers every cause on this page in the order Claude encounters them during a connection. Run through it before filing an issue.
134144 
135145<Steps>
136146 <Step title="Public DNS resolution">
137 From a network outside your own, confirm `dig +short your-server.example.com` returns a globally-routable address.
147 From a network outside your own, confirm `dig +short your-server.example.com` returns a globally routable address.
138148 </Step>
139149 
140150 <Step title="Public reachability">
141 From a public network, confirm `curl -i https://your-server.example.com/your-mcp-path` returns a response (a `401` or `405` is fine; a timeout or connection refused is not).
151 From a public network, confirm `curl -i https://your-server.example.com/your-mcp-path` returns a response. A `401` or `405` is fine, and a timeout or connection refused is not.
142152 </Step>
143153 
144154 <Step title="No redirect">
from line 160
150160 </Step>
151161 
152162 <Step title="Discovery metadata">
153 Confirm `/.well-known/oauth-protected-resource` returns `200` with valid JSON, and that one of `/.well-known/oauth-authorization-server` or `/.well-known/openid-configuration` does the same — only one is needed. The authorization server metadata should include a `registration_endpoint` (DCR) or advertise `"client_id_metadata_document_supported": true` (CIMD), or you should be using pre-registered credentials, and it should advertise `"code_challenge_methods_supported": ["S256"]`.
163 Confirm `/.well-known/oauth-protected-resource` returns `200` with valid JSON, and that one of `/.well-known/oauth-authorization-server` or `/.well-known/openid-configuration` does the same. Only one is needed. The authorization server metadata should include a `registration_endpoint` (DCR) or advertise `"client_id_metadata_document_supported": true` (CIMD), or you should be using pre-registered credentials, and it should advertise `"code_challenge_methods_supported": ["S256"]`.
154164 </Step>
155165 
156166 <Step title="Cross-host hint">
157 If your authorization server is on a different host than your MCP server, confirm your protected resource metadata's `authorization_servers` field points at it, and that the authorization server's host is reachable from Anthropic's egress range and answers `/.well-known/openid-configuration` (or `/.well-known/oauth-authorization-server`). See [cross-host authorization servers](/docs/connectors/building/authentication#cross-host-authorization-servers).
167 If your authorization server is on a different host than your MCP server, confirm your protected resource metadata's `authorization_servers` field points at it, and that the authorization server's host is reachable from Anthropic's egress range and answers `/.well-known/openid-configuration` or `/.well-known/oauth-authorization-server`. See [Serve discovery metadata](/docs/connectors/building/authentication#serve-discovery-metadata).
158168 </Step>
159169 
160170 <Step title="Collect the reference ID">
161 Reproduce the failure and copy the `ofid_` value from the error URL. Include it, your server URL, and your server-side logs in your report.
171 Reproduce the failure and copy the `ofid_` value from the error URL, then [report the problem](#report-the-problem-to-anthropic) with it.
162172 </Step>
163173</Steps>
164174 
165## Related topics
175## Report the problem to Anthropic
166176 
167<Columns cols={2}>
168 <Card title="Authentication" icon="lock" href="/docs/connectors/building/authentication">
169 OAuth requirements and supported auth types.
170 </Card>
177If the checklist doesn't find the cause, report the failure on the [`anthropics/claude-ai-mcp` issue tracker](https://github.com/anthropics/claude-ai-mcp/issues). When a connection or sign-in fails on claude.ai, the error message and the page URL include a reference ID that starts with `ofid_`, for example:
171178 
172 <Card title="Testing" icon="flask" href="/docs/connectors/building/testing">
173 How to test your server before publishing.
174 </Card>
179```text theme={null}
180.../customize/connectors?step=start_error&flow_id=ofid_d32594c73257a651
181```
175182 
176 <Card title="Lazy authentication" icon="hourglass" href="/docs/connectors/building/lazy-authentication">
177 The 401 + WWW-Authenticate discovery handshake.
178 </Card>
183Include that ID, your server URL, and what your server-side logs show during the attempt. The ID lets Anthropic trace the exact failure on its side, and it's time-limited, so report soon after the failure. For a tool-call failure there's no reference ID; include the tool name, the time of the failure in UTC, and the connector URL instead.
179184 
180 <Card title="IP address reference" icon="network-wired" href="https://platform.claude.com/docs/en/api/ip-addresses">
181 Anthropic's published IP ranges for allowlisting.
182 </Card>
183</Columns>
185## Related resources
186 
187* [Authentication for connectors](/docs/connectors/building/authentication): OAuth requirements and supported auth types
188* [Lazy authentication](/docs/connectors/building/lazy-authentication): the `401` and `WWW-Authenticate` discovery handshake
189* [Test your connector](/docs/connectors/building/testing): how to test your server before publishing
190* [IP address reference](https://platform.claude.com/docs/en/api/ip-addresses): Anthropic's published IP ranges for allowlisting
184191 
Feedback