---
title: "MCP Server"
description: "Expose your agent-native app as a remote MCP server so Claude, ChatGPT, Claude Code, Cursor, and other AI tools can call your app's actions directly."
---

# MCP Server

**This page: the lower-level MCP server reference.** How every agent-native app exposes its actions over MCP — the auto-mounted endpoint, auth modes, the `tools/call` / `ask-agent` surface, and custom mounting. Reach for it when you need server internals; for connecting a host, start with [External Agents](/docs/external-agents).

| If you want to…                                              | Read                                     |
| ------------------------------------------------------------ | ---------------------------------------- |
| Connect an external agent/host to your app                   | [External Agents](/docs/external-agents) |
| Give your agent more tools (consume other MCP servers)       | [MCP Clients](/docs/mcp-clients)         |
| Build inline UIs that render in Claude/ChatGPT               | [MCP Apps](/docs/mcp-apps)               |
| Build generated inline UI inside Agent-Native chat           | [Generative UI](/docs/generative-ui)     |
| Lower-level MCP server reference (auth, tools, custom mount) | **This page** — MCP Server               |

Every agent-native app automatically exposes a remote MCP (Model Context Protocol) server, so external AI tools like Claude, ChatGPT custom MCP apps, Claude Code, Cursor, Codex, and VS Code GitHub Copilot can discover and call your app's actions directly — no extra code needed. If your goal is to _connect_ one of those hosts to a hosted app, [External Agents](/docs/external-agents) covers the recommended single Dispatch connector, per-app URLs, OAuth, MCP Apps inline UIs, and deep links. This page documents what sits underneath that.

## Overview {#overview}

MCP is the standard protocol for connecting AI tools to external capabilities. When you deploy an agent-native app, it auto-mounts an MCP endpoint alongside the existing A2A endpoint. Any MCP-compatible host can connect and use your app's tools.

Key concepts:

- **Auto-mounted** — every app gets `/mcp` for free, no setup required
- **Streamable HTTP** — uses the modern MCP transport over standard HTTP (POST + SSE)
- **Same actions** — the exact same action registry that powers agent chat and A2A
- **`ask-agent` tool** — a meta-tool that delegates to the full agent loop for complex tasks
- **MCP Apps** — actions can advertise interactive UI resources through the official `io.modelcontextprotocol/ui` extension
- **Standard remote MCP OAuth** — OAuth 2.1 discovery, dynamic client registration, authorization-code + PKCE, refresh-token rotation
- **Bearer auth fallback** — uses `ACCESS_TOKEN`, `ACCESS_TOKENS`, or connect-minted JWTs for clients that cannot run OAuth

<Diagram id="doc-block-1362vx8" title="Your app as an MCP server" summary={"External hosts connect over Streamable HTTP. Each action is one tool; ask-agent delegates to the full agent loop."}>

```html
<div class="diagram-mcp">
  <div class="diagram-col">
    <div class="diagram-node">Claude</div>
    <div class="diagram-node">ChatGPT</div>
    <div class="diagram-node">Cursor · Codex</div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel">
    <span class="diagram-pill accent">POST /mcp</span
    ><small class="diagram-muted">Streamable HTTP</small
    ><small class="diagram-muted"
      >initialize &rarr; tools/list &rarr; tools/call</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-col">
    <div class="diagram-box" data-rough>
      each action<br /><small class="diagram-muted">= one tool</small>
    </div>
    <div class="diagram-box" data-rough>
      ask-agent<br /><small class="diagram-muted">&rarr; full agent loop</small>
    </div>
  </div>
</div>
```

```css
.diagram-mcp {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.diagram-mcp .diagram-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.diagram-mcp .diagram-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.diagram-mcp .diagram-arrow {
  font-size: 20px;
  line-height: 1;
}
```

</Diagram>

## MCP vs A2A {#mcp-vs-a2a}

Both protocols are auto-mounted. Use whichever fits your use case:

|                    | MCP                                                                      | A2A                                          |
| ------------------ | ------------------------------------------------------------------------ | -------------------------------------------- |
| **Best for**       | External tools calling your app                                          | Agent-to-agent communication                 |
| **Protocol**       | MCP Streamable HTTP                                                      | JSON-RPC 2.0                                 |
| **Tool discovery** | `tools/list`                                                             | Agent card at `/.well-known/agent-card.json` |
| **Endpoint**       | `/mcp`                                                                   | `/_agent-native/a2a`                         |
| **Supported by**   | Claude, ChatGPT, Claude Code, Cursor, Codex, Cowork, and other MCP hosts | Other agent-native apps                      |
| **Execution**      | Direct tool calls (no extra LLM)                                         | Full agent loop (LLM reasoning)              |

You can also use the `ask-agent` MCP tool to get the best of both worlds — call it from Claude Code and let your app's agent reason through complex tasks.

## Manual MCP host config {#manual-config}

For the recommended one-command setup, use [External Agents](/docs/external-agents). If you are hand-writing MCP config for an OAuth-capable host, add your app as a remote MCP server with no static headers:

```bash
claude mcp add --transport http mail https://mail.example.com/mcp
```

Or write the entry by hand in `.mcp.json` (project scope) or `~/.claude.json` (user scope):

```jsonc
// .mcp.json
{
  "mcpServers": {
    "mail": {
      "type": "http",
      "url": "https://mail.example.com/mcp",
    },
  },
}
```

Then run `/mcp` in Claude Code and choose **Authenticate**. For clients that cannot perform remote MCP OAuth, use the Connect page or a static bearer-token entry with `headers.Authorization`. Once authenticated, you can use your app's tools naturally:

```
> draft an email to John about the Q3 report

Claude Code calls: draft-email(to: "john@example.com", subject: "Q3 Report", body: "...")
```

## Connecting from other MCP hosts {#other-clients}

Any MCP host that supports Streamable HTTP transport can connect. The endpoint is:

```
POST https://your-app.example.com/mcp
```

The server supports the standard MCP handshake: `initialize` → `initialized` → `tools/list` → `tools/call`.

<Endpoint id="doc-block-86ic5g" title="MCP endpoint" summary="The auto-mounted Streamable HTTP endpoint every agent-native app exposes." method="POST" path="/mcp" auth="Standard remote MCP OAuth (Bearer access token), connect-minted JWT, or static ACCESS_TOKEN/ACCESS_TOKENS" params={[
  {
    "name": "Authorization",
    "in": "header",
    "type": "string",
    "required": false,
    "description": "Bearer access token. Required except for loopback local-dev probes."
  },
  {
    "name": "method",
    "in": "body",
    "type": "string",
    "required": true,
    "description": "MCP method, e.g. initialize, tools/list, tools/call."
  }
]} request={{
  "contentType": "application/json",
  "example": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"ask-agent\",\n    \"arguments\": { \"message\": \"Summarize Q3 signups by source\" }\n  }\n}"
}} responses={[
  {
    "status": "200",
    "description": "MCP result (POST + SSE)."
  },
  {
    "status": "401",
    "description": "Unauthenticated — responds with a WWW-Authenticate header pointing at OAuth discovery."
  }
]}>

Auto-mounted on every app. Speaks the standard MCP handshake (`initialize` → `initialized` → `tools/list` → `tools/call`) plus `resources/list`, `resources/templates/list`, and `resources/read` when an action declares `mcpApp`. Each action maps to one tool; `ask-agent` delegates to the full agent loop.

</Endpoint>

If an action declares `mcpApp`, the server advertises the official MCP Apps extension and supports the resource routes needed to render it inline:

- Advertises `io.modelcontextprotocol/ui` and supports `resources/list`, `resources/templates/list`, and `resources/read` for the app resource.
- Hosts that render MCP Apps show the UI inline; hosts that don't can still call the tool and use the deep-link fallback.
- Product UIs should use `embedApp()` so the inline surface is the real React app route, or a focused route that renders a shared React component such as an Analytics chart — never a separate plain-HTML implementation.
- The server emits both standard MCP Apps metadata and ChatGPT Apps SDK compatibility metadata so app-capable hosts find the same `ui://` resource.
- The current official extension matrix includes Claude, Claude Desktop, VS Code GitHub Copilot, Goose, Postman, MCPJam, ChatGPT, and Cursor. Host support varies by version and plan — see the [External Agents Catalog — MCP Apps compatibility](/docs/external-agents-catalog#mcp-apps-compatibility) for the user-facing guidance.

### MCP App embed bridge {#mcp-app-embed-bridge}

`embedApp()` is the low-level URL-first MCP App helper: it launches a signed app
route inline through transplant (Claude), controlled-frame (ChatGPT), or direct
navigation, mediates host actions over the `ui/*` JSON-RPC bridge (and the
`agentNative.mcpHost.*` postMessage relay for the controlled-frame path), and
clamps the resource shell height so a full-app route does not render as an
oversized chat artifact.

See [MCP Apps](/docs/mcp-apps#mcp-app-bridge) for the full embed bridge details — transplant vs controlled-frame, the `ui/*` and postMessage tables, `create_embed_session` / `embedStartUrl`, CSP and domain rules, extension `srcDoc` embedding, height clamping, and the host bridge client API.

## Tools {#tools}

Every caller gets a **compact catalog by default** (template-declared app actions plus the cross-app builtins), with the full action surface served only on explicit opt-in and `tool-search` always available to reach the rest. See [External Agents Catalog → Catalog tiers](/docs/external-agents-catalog#catalog-tiers) for the full explanation.

Each action maps directly to one MCP tool:

| Action property    | MCP tool property |
| ------------------ | ----------------- |
| `tool.description` | `description`     |
| `tool.parameters`  | `inputSchema`     |
| Action name        | Tool name         |

When `mcpApp` is present, the tool entry also includes `_meta.ui.resourceUri`, `_meta["ui/resourceUri"]`, and `_meta["openai/outputTemplate"]`, and the corresponding `ui://` resource is returned as `text/html;profile=mcp-app`.

### The `ask-agent` tool {#ask-agent}

In addition to individual action tools, every MCP server includes an `ask-agent` meta-tool. This sends a natural-language message to the app's AI agent and returns the response.

Hosted MCP requests use a bounded inline wait so a long agent loop does not
keep one `tools/call` open until a client or gateway timeout. The optional
`async` argument starts the durable task and returns immediately; without it,
the server waits briefly for a fast completion and then returns a task handle
when more work remains. The hosted wait is capped at 25 seconds.

Use `ask-agent` for complex tasks that benefit from the agent's reasoning and context:

```json
{
  "name": "ask-agent",
  "arguments": {
    "message": "Draft a follow-up email to the Q3 planning thread with John, summarizing the action items we discussed",
    "async": true
  }
}
```

The agent runs the same loop as the interactive chat — it can call multiple tools, reason about context, and produce a thoughtful response. A task that is not finished returns JSON containing `taskId`, `status`, `pollAfterMs`, and:

```json
{
  "poll": {
    "tool": "ask_app_status",
    "arguments": { "app": "mail", "taskId": "task_123" }
  }
}
```

Call `ask_app_status` with those arguments until the task reaches a terminal
state. Dispatch's app-owned `ask_app` uses the same durable task and status
contract, with the selected-app grant checked on both submission and polling.

If a bounded transient status read is unavailable, `ask_app_status` can return
`status: "unknown"`, `statusRead: "unavailable"`, `retryable: true`, the
original `app` and `taskId`, and `pollAfterMs` / `poll`. This means the status
read is unavailable—not that the durable task failed. Retry `ask_app_status`
with that exact `app` and `taskId`; never resubmit `ask_app` to recover, because
submission can duplicate work. Permanent authentication, not-found, and
protocol failures remain errors.

## Authentication {#authentication}

The MCP endpoint supports standard remote MCP OAuth plus the existing bearer-token fallback:

| Mode                        | How it works                                                                                                          |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Standard MCP OAuth          | Client discovers auth from `WWW-Authenticate`, registers, runs PKCE, and sends `Authorization: Bearer <access-token>` |
| Connect-minted JWT          | `npx @agent-native/core@latest connect` / the Connect page mints a per-user, revocable JWT                            |
| `ACCESS_TOKEN`              | Static bearer token — client sends `Authorization: Bearer <token>`                                                    |
| `ACCESS_TOKENS`             | Comma-separated list of valid static bearer tokens                                                                    |
| `A2A_SECRET`                | JWT-based auth — tokens are verified cryptographically                                                                |
| _(none set, loopback only)_ | No auth required for local dev probes                                                                                 |

For OAuth-capable MCP hosts, configure the remote server URL with no static headers:

```bash
claude mcp add --transport http agent-native https://dispatch.agent-native.com/mcp
```

The first unauthenticated MCP request receives:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://dispatch.agent-native.com/.well-known/oauth-protected-resource", scope="mcp:read mcp:write mcp:apps offline_access"
```

Discovery endpoints:

| Endpoint                                  | Purpose                                     |
| ----------------------------------------- | ------------------------------------------- |
| `/.well-known/oauth-protected-resource`   | RFC 9728 protected-resource metadata        |
| `/.well-known/oauth-authorization-server` | OAuth authorization server metadata         |
| `/mcp/oauth/register`                     | Dynamic public-client registration          |
| `/mcp/oauth/authorize`                    | Browser authorization + consent             |
| `/mcp/oauth/token`                        | Authorization-code and refresh-token grants |

<Diagram id="doc-block-79kjly" title="OAuth discovery flow" summary={"A 401 kicks off discovery, registration, and a PKCE authorize → token exchange. The Bearer token is audience-bound and scoped."}>

```html
<div class="diagram-oauth">
  <div class="diagram-box" data-rough>
    first request<br /><small class="diagram-muted">no token</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-pill warn">401 · WWW-Authenticate</div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel">
    <span class="diagram-pill">/.well-known/oauth-protected-resource</span
    ><span class="diagram-pill">/.well-known/oauth-authorization-server</span
    ><small class="diagram-muted">discover</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-col">
    <div class="diagram-pill">register</div>
    <div class="diagram-pill">authorize (PKCE)</div>
    <div class="diagram-pill">token</div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box" data-rough>
    Bearer access token<br /><small class="diagram-muted"
      >audience-bound · mcp:read / write / apps</small
    >
  </div>
</div>
```

```css
.diagram-oauth {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.diagram-oauth .diagram-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.diagram-oauth .diagram-col {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.diagram-oauth .diagram-arrow {
  font-size: 20px;
  line-height: 1;
}
```

</Diagram>

Access tokens are signed JWTs whose audience is the exact MCP resource URL. The server accepts only tokens issued for itself and applies scopes before listing/calling tools:

| Scope       | Allows                                      |
| ----------- | ------------------------------------------- |
| `mcp:read`  | read-only actions                           |
| `mcp:write` | mutating actions and `ask-agent`            |
| `mcp:apps`  | MCP Apps resources (`ui://` HTML resources) |

Refresh tokens are stored only as hashes and are rotated on every refresh. `npx @agent-native/core@latest connect` writes this URL-only OAuth entry for Claude Code clients by default; keep the Connect page, `npx @agent-native/core@latest connect --token <token>`, and static bearer config for local stdio proxying, older clients, and emergency/debug flows.

## Custom MCP setup {#custom-setup}

The MCP server is auto-mounted by the agent-chat plugin. For most apps, no configuration is needed. If you need custom behavior, you can mount it manually in a server plugin:

```ts
// server/plugins/mcp.ts
import { mountMCP } from "@agent-native/core/mcp";
import { autoDiscoverActions } from "@agent-native/core/server";

export default defineNitroPlugin(async (nitro) => {
  const actions = await autoDiscoverActions(import.meta.url);

  mountMCP(nitro, {
    name: "My App",
    description: "Custom MCP server",
    actions,
    // Optional: provide ask-agent handler
    askAgent: async (message) => {
      // Your custom agent logic
      return "Response";
    },
    // Optional: override the route prefix (default "/_agent-native")
    // routePrefix: "/_agent-native",
  });
});
```

## Example: analytics from Claude Code {#example}

You have a deployed analytics app at `analytics.example.com`. From Claude Code:

```bash
claude mcp add --transport http analytics https://analytics.example.com/mcp
```

Or add it by hand in `.mcp.json`:

```jsonc
// .mcp.json
{
  "mcpServers": {
    "analytics": {
      "type": "http",
      "url": "https://analytics.example.com/mcp",
    },
  },
}
```

Now in Claude Code:

```
> How many signups did we get last week?

Claude Code calls: run-query(sql: "SELECT count(*) FROM signups WHERE created_at > now() - interval '7 days'")
→ "1,247 signups last week"
```

For more complex analysis:

```
> Ask the analytics agent to prepare a full breakdown of Q3 signups by source, with trends

Claude Code calls: ask-agent(message: "Prepare a full breakdown of Q3 signups by source, with trends")
→ The analytics agent runs multiple queries, reasons about the data, and returns a formatted report
```

## What's next {#whats-next}

- [**External Agents**](/docs/external-agents) — the recommended connect flow and per-host setup
- [**External Agents Catalog**](/docs/external-agents-catalog) — catalog tiers and tool exposure for template authors
- [**MCP Apps**](/docs/mcp-apps) — inline UI resources for hosts that render them
- [**MCP Clients**](/docs/mcp-clients) — the other direction: your app consuming external MCP servers
- [**A2A Protocol**](/docs/a2a-protocol) — the sibling protocol for agent-to-agent calls
- [**Actions**](/docs/actions) — how an action becomes an MCP tool
