> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usepatchwork.co/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP export

> The other door on a published agent. Cursor, Claude and ChatGPT get its tools — as the signed-in user, through your own login.

A published [agent](/guides/agents) already runs inside your product. MCP export opens a second door on that same agent: your users can reach its tools from their own AI client — Cursor, Claude, ChatGPT — acting as themselves, with the permissions they already have.

You do not run an MCP server. You do not write a second tool catalog. The tools are the ones you already wired, and the only thing you host is a [login bridge](/guides/mcp-login-bridge) so your users authenticate with **you**.

## What travels through the door

The capabilities, not the assistant.

| | In your app | In their client |
| - | - | - |
| What they get | the agent — instructions, knowledge, memory, the loop | the wired tools |
| Who they are | your user, by subject or session token | the same user, after logging into you |
| Who calls your backend | us, as that user | us, as that user |
| What you can read back | the run and its trace | the same run, marked `mcp` |

The client already has a model, so we export no persona, no knowledge, no memory, and none of the [built-in primitives](/guides/agents#what-every-agent-can-do). Nothing about your agent's character crosses over.

## The URL

```
POST https://api.usepatchwork.co/mcp/agents/agent_4f3ca81b9d2e7f60…
```

The path carries the agent's public id — the same string your threads name, pinning one exact published version. There is no secret in the URL and no query string; the client authenticates with a bearer token it obtains itself.

| | |
| - | - |
| Transport | Streamable HTTP, single `POST`. `GET` and `DELETE` return `405`. |
| Methods | Tools only — `tools/list` and `tools/call`. No prompts, resources, or subscriptions. |
| Catalog changes | `tools.listChanged` is `false`. Clients re-list rather than being pushed to. |
| Older clients | A client that still opens with `initialize` is served from the same URL, from the same catalog. |

Paste that URL into any client that speaks Streamable HTTP. For Cursor:

```json theme={null}
{
  "mcpServers": {
    "acme": {
      "url": "https://api.usepatchwork.co/mcp/agents/agent_4f3ca81b9d2e7f60…"
    }
  }
}
```

No `headers` block. The client discovers how to authenticate from our `401`.

## Turning it on

Two things, both of which you may already have:

<Steps>
  <Step title="Publish the agent">
    Publishing mints the public id, and the MCP URL exists from that moment. The dashboard shows it on the published agent with a paste-ready snippet.
  </Step>

  <Step title="Give its connection a login URL">
    Set **MCP login URL** on the [connection](/guides/connections) the agent uses. That is your [login bridge](/guides/mcp-login-bridge) — the one page you host, which authenticates your user and hands us back a signed subject.
  </Step>
</Steps>

Until the connection carries that URL the door stays shut: discovery returns `404` and authorization is refused. Identity is the whole requirement — there is no MCP infrastructure for you to stand up.

<Note>
  The login URL lives on the connection, not the agent. Two agents pointing at different connections can serve different user pools; agents on the same connection share one.
</Note>

## Who the user is

**You own identity. We run the OAuth machinery.** Patchwork is both the resource server that hosts the MCP URL and the authorization server the client talks to. We never see your users' credentials, and we never invent a user — the one step only you can do, proving who someone is, is the one step we hand back to you.

```
client ──1── POST /mcp/agents/…                    401 + where to authorize
client ──2── our discovery + registration + authorize
   we  ──3── 302 to your login bridge              you authenticate your user
  you  ──4── 302 back with a signed subject        we verify it against your key
   we  ──5── consent screen → grant → code
client ──6── our token endpoint                     an access token, ours
client ──7── POST /mcp/agents/… with that token    tools/list, tools/call
```

Step 4 is your entire surface. Everything else — discovery, client registration, consent, tokens, grants, revocation — is ours.

### Discovery endpoints

A client follows these on its own; they are listed so you can see what it will touch.

| Endpoint | What it says |
| - | - |
| `GET /.well-known/oauth-protected-resource/mcp/agents/{id}` | This resource, and that we are its authorization server. `404` when the agent is not exportable. |
| `GET /.well-known/oauth-authorization-server` | Our endpoints, `authorization_code` only, PKCE `S256` required. |
| `POST /oauth/register` | Dynamic client registration, so a client needs no credentials from you. |
| `GET /oauth/authorize` | Resolves the agent, then redirects to your bridge. |
| `POST /oauth/token` | Exchanges the code for the access token. |

### Two consents, cleanly split

* **Which tenant am I, and may I be here at all** — your bridge, your policy, baked into the subject.
* **May this client act as me on this agent** — our consent screen, which records the grant.

## What exports

`tools/list` is the agent's wired tools, filtered, with each one projected like this:

| MCP field | From the patch |
| - | - |
| `name` | the patch key |
| `title` | its title |
| `description` | its description |
| `inputSchema` | its input schema, verbatim |
| `outputSchema` | its output schema, when it declares one |
| `annotations.readOnlyHint` | `true` for a read, `false` for a write — always set explicitly |

Tools come back sorted by key. Any `@` mention in a title or description is rewritten to a plain label first, so the client's model is never told to load knowledge it cannot reach or to call a tool absent from its own list.

A tool is exported only if **all** of these hold:

| Requirement | Otherwise |
| - | - |
| The agent is published and its connection has a login URL | the whole door is closed |
| The patch is wired on the agent and resolvable in your workspace | omitted |
| It is not a built-in primitive | omitted |
| Its interaction is silent — it runs server-side and returns a result | omitted |
| Its permission is **Always allow** | omitted |
| Its effective [risk](/guides/patches#risk) is not `high` or `critical` | omitted |

### Writes export, but only deliberately

A write is exported the same as a read, and it really runs — as the user, against your backend, under your own authorization. What stops a write from leaking out is the permission on the wiring.

<Warning>
  **Always ask** does not export. A patch wired that way pauses a run for a human decision, and this door has no way to ask yet — so rather than run it unconfirmed, we leave it out of the catalog. A write reaches an MCP client only when you wire it **Always allow**.
</Warning>

Because [risk can depend on arguments](/guides/patches#risk), a tool can pass the catalog filter and still escalate on a particular call. That call is refused with `confirmation_required` rather than run without a confirmation nobody can give.

## Choosing a connection

A client that can send headers may name one per call:

```
Patchwork-Connection: production
```

A uuid or a name, as in [relay](/guides/loom-relay). Without the header we use the connection pinned to the agent. A customer patch that resolves no connection fails that tool, not the session.

## Every call is a run

A `tools/call` opens a [run](/guides/runs), dispatches the tool, and closes it. The conversation stayed in the client, so the run has no model turn — but everything else is the same spine you already read:

* `source` is `mcp` on the run, and the dashboard badges it so it does not read as a broken chat.
* The [tool call](/guides/tool-calls) is recorded with redacted arguments and result, risk, and timing, and `GET /v1/loom/runs/:id/trace` reads back as usual.
* [Events](/guides/events#the-envelope) carry `origin: "mcp"`, and a [webhook endpoint](/api-reference/webhook-endpoint) can subscribe to that origin alone.
* The subject on the run is the one your bridge asserted, so you can answer "what did this user do from their own client" without a second log.

Each call gets its own throwaway thread, because there is no conversation on our side to thread. Those are left out of `GET /v1/loom/threads` so your chat history stays chat history — read the runs instead.

## Managing access

The grant lives with us, which is what makes revocation immediate. List and revoke with your workspace API key:

```bash theme={null}
curl https://api.usepatchwork.co/v1/mcp/grants?subject=user_9f2 \
  -H "Authorization: Bearer sk_…"

curl -X DELETE https://api.usepatchwork.co/v1/mcp/grants/2c5ea4c0-… \
  -H "Authorization: Bearer sk_…"
```

Revoking kills the grant and every access token issued against it, so the next call from that client fails at our door. See [The MCP grant object](/api-reference/mcp-grant). Build your own "connected apps" screen on these two endpoints — we ship no UI for your users.

## Scopes

Consent asks for `read` and `write`, and the granted scopes are recorded on the grant for you to read back.

<Note>
  The exported catalog does not currently narrow with the scope. What a client can call is set by the agent's wiring — the permission and risk filters above — not by what it asked for at consent. Wire the agent to the smallest set you want reachable.
</Note>

## Errors

Errors split cleanly in two. Anything about **the door** is an HTTP status, because the client has to do something about it:

| What happened | What the client sees |
| - | - |
| No token, or one expired, revoked, or issued for another agent | `401` with `WWW-Authenticate` pointing at discovery, so the client re-authorizes |
| Unknown agent, or one that is draft, archived, or has no login URL on its connection | `404`, with no hint about which — an unexportable agent does not advertise its existence |
| `GET` or `DELETE` on the URL | `405` |

Anything about **a tool** comes back as a normal MCP result with `isError: true` and the code inside, so the client keeps its session and can try something else:

| `error` in the result | When |
| - | - |
| `unknown_tool` | The named tool is not in this agent's exported set. |
| `confirmation_required` | Risk escalated on these arguments, and this door cannot ask. |
| `insufficient_credits` | Your workspace is out of credit. |
| a description of the failure | The tool ran and your endpoint refused, or no connection resolved for it. |

Either way the [trace](/guides/traces) holds the real detail — the arguments, the egress request, and the failure — which is where to look rather than at what the client printed.

## What is not supported yet

* **Elicitation and confirmation.** Hence no **Always ask** tools, and no `proposed_action` or handoff patches, which assume your frontend.
* **Refresh tokens.** An access token lasts an hour; after that the client runs the authorization flow again.
* **API keys on this door.** OAuth only — there is no paste-a-key path.
* **Custom domains.** The URL is on our host.
* **Several connections per exported agent.** One agent, one connection.
* **Prompts, resources, subscriptions, and server-pushed catalog changes.**


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.