One read of Claude Documentationclaude-docs-20260925T180706Z
76 pages moved out of 255 read.
What this read moved
1-25 of 76, page 1 of 4This 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
claude-science/core-concepts Changed · +8 / -8 lines
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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