# Hosted MCP server

Source: https://docs.settlemint.com/docs/api-reference/cli/hosted-mcp
Connect Claude, ChatGPT or another MCP app to DALP at /api/mcp: read tools, and prepare tools that post a draft to sign or run a write under an agent's mandate.

The hosted MCP server lets a member connect an AI app that speaks MCP, the Model Context Protocol, to DALP. Claude and ChatGPT are examples. The server runs inside DALP at `/api/mcp`, so the app needs no CLI and no API key. The app reads what the member can read. By default it prepares changes as drafts that the member opens and signs. With `execution` set to `mandate`, DALP instead runs a mint, burn, pause, resume, block or unblock from one of the organisation's agent accounts, inside that agent's on-chain mandate and with no person signing, as [Execution modes](#execution-modes) describes. The app never signs, and the member's key never signs for it.

This page covers the hosted server. The `dalp --mcp` server described in [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) is a different server, started on a person's own machine, and it is unaffected by the hosted server. Choose the hosted server when the member uses a chat app and the organisation wants a per-member grant it can see and revoke. Choose `dalp --mcp` when an agent runs next to a shell with the API key from `dalp login`.

[Figure] A member connects an app, and the app reads or prepares drafts the member signs. An organisation owner switches the MCP server on. The organisation's assistant must be on too, and so must the platform-wide assistant switch, which is on by default. The app finds the OAuth sign-in from the metadata at /api/mcp. The member signs in, picks the organisation and allows read and prepare. Each tool call runs as that member in that organisation. A read tool returns what the member can read. A prepare tool posts a draft to the member's Actions inbox. The member reviews the draft and signs it with wallet verification. The app never signs. A prepare call with execution set to mandate skips the draft: one of the organisation's agents runs the write under its on-chain mandate. Labels: read tool; prepare_* tool; Switch the server on; Owner; The assistant must; be on too; Finds the sign-in; App; OAuth metadata at; /api/mcp; Allows the app; Member; Picks the organisation,; allows read and prepare; Runs as the member; Tool call; In the one organisation the grant binds to; Returns data; Read; What the member can read; and nothing else; Posts a draft; Prepare; To the member's Actions inbox; Nothing is signed yet; Reviews and signs; Member; Signs with wallet verification.

An owner switches the MCP server on, the member signs the app in over OAuth, and each tool call then reads as that member or posts a draft the member signs.

## Switch the server on [#switch-the-server-on]

An organisation owner or a platform administrator switches the server on. It is off by default. The platform-wide assistant switch, which is on by default, must be on too. While the server is off, or while either assistant switch is off, every request to `/api/mcp` answers 403 with `mcp_server_off`. Sign-in and consent still complete, but the app gets no data.

1. Open the Assistant page in organisation settings.
2. Make sure **Assistant in search** is on. The MCP server needs the assistant, and with the assistant off the "Connected apps" list is hidden.
3. Turn on **MCP server**.

The same two roles are the only ones who can switch it off, in the Console or through the Platform API. The Assistant page appears only in their navigation, and another member who opens its address cannot change the switch and sees "Only an owner can change this setting."

When an owner turns the switch off again, every request to `/api/mcp` for that organisation answers 403 with `mcp_server_off` until an owner turns it back on.

## Connect an app [#connect-an-app]

Give the app the server URL. Replace `<your DALP address>` with the address your organisation uses for the Console:

```text
<your DALP address>/api/mcp
```

The app finds the sign-in by itself. The server answers a request without a valid token with 401 and a `WWW-Authenticate` header that points to its protected-resource metadata. The metadata names DALP's OAuth issuer, and the app registers itself as an OAuth client without signing in, so no one creates a client by hand and DALP does not check who the app is. The registration requires PKCE, the OAuth Proof Key for Code Exchange extension that binds each sign-in to the app that started it. An access token lasts one hour and a refresh token lasts 30 days.

The metadata is served by the Platform API at `/.well-known/oauth-protected-resource/api/mcp`. The `WWW-Authenticate` header of the 401 carries the exact address, and an app that follows it reads this document:

```json
{
  "resource": "https://dalp.northwind.example/api/mcp",
  "authorization_servers": ["https://dalp.northwind.example/api/auth"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["dalp:read", "dalp:prepare"],
  "resource_name": "DALP"
}
```

The Console forwards `/.well-known/*` to the API, as it does `/api/*` and `/dalp/*`, so the Console address serves this metadata. A gateway in front of the Console must pass `/.well-known` through as well.

The server is stateless and speaks streamable HTTP. An app sends JSON-RPC requests with `POST`, and a `GET` to `/api/mcp` answers 405 with "This MCP server is stateless: POST JSON-RPC requests to it."

### What the member does [#what-the-member-does]

1. The app opens a sign-in page titled with the app's name, such as **Sign in to connect Claude**. This page offers no account creation. The member signs in, including two-factor verification, and returns to the app's request.
2. The consent page, titled with the app's name, such as "Connect Claude to DALP", shows the **Organisation** the grant binds to. A member of several organisations picks one with "Choose an organisation", and the page reloads with the choice.
3. Under **The app will be able to**, the page lists what the app asked for: "Read the DALP data you can see", "Prepare actions for you to review in your Actions inbox", or both. With "Prepare actions" it also lists "Have your organisation's agent principal, if it has one, run a mint, burn, pause, resume, block or unblock within its mandate, with no one signing", which is `execution` set to `mandate`, as [Execution modes](#execution-modes) describes. The page then states: "It cannot sign anything with your account."
4. The member selects **Allow** or "Deny". A member with no organisation cannot select "Allow", and the page says "You are not a member of an organisation." A failed answer keeps the member on the page with "Your answer could not be sent. Try again."

The name on both pages is the name the app gave itself, so allow only apps you trust. A request that lost its signed parameters shows "This connection request is incomplete" and offers no answer. When the server is off, an owner sees "The MCP server is off" with a link, **Open assistant settings**. A member sees no notice, because only an owner can change the switch.

The grant belongs to one member in one organisation. The app works in that organisation only, with that member's permissions.

## Read tools [#read-tools]

The server offers the same four tools as `dalp --mcp`. An app finds a tool with `search_tools`, reads its schema with `get_tool_details`, and runs a read with `call_read_tool` or a prepare tool with `call_write_tool`. The view tool `show_holders` is listed on its own.

The read tools are the CLI's `get` commands, the same set `dalp --mcp` offers through `call_read_tool`, described in [MCP tools](https://docs.settlemint.com/docs/api-reference/cli/ai-agents#mcp-tools). Every read runs as the member who signed the app in, in the organisation the grant binds to. The app sees what the member sees and nothing else. A read returns the route's own response body, without the masking the Console assistant applies, so names, wallet addresses, balances and identity data in it reach the app and the model provider behind it. The organisation's assistant data settings do not limit what the hosted server returns.

The access token is read-only on the API. A write method sent with it answers 403 `SESSION_POLICY_DENIED` (DALP-0036). A route that only the Console may call refuses the token with the same code, whatever the method. A write goes through a prepare tool instead, as a draft for a person to confirm in DALP or, under a mandate, as a write one of the organisation's agents runs.

## Prepare tools [#prepare-tools]

Each `prepare_*` tool matches one Console control, such as the controls on an asset's page, and is named after it, for example `prepare_asset_pause`. A prepare tool takes the asset's contract address as `assetId`, the control's own fields and an optional `execution`.

`search_tools` and `get_tool_details` return each prepare tool's MCP annotations, the hints that tell an app what a tool can change. `prepare_asset_mint`, `prepare_asset_burn`, `prepare_asset_pause`, `prepare_asset_unpause`, `prepare_asset_blocklist_block` and `prepare_asset_blocklist_unblock` carry `destructiveHint: true`, because with `execution` set to `mandate` they run the write at once and no person signs. The hint stays the same when a call uses `inbox`. Every other prepare tool carries `destructiveHint: false`. It answers `mandate` with `execution-unsupported`, so it can only post a draft. No prepare tool is marked read-only or idempotent, because an `inbox` call takes no idempotency key. The app runs every prepare tool through `call_write_tool`, which is marked destructive whatever tool it runs.

A prepare tool checks the address and each field, reads the asset as the member, and refuses when the member does not hold the permission the control needs. When the checks pass, the platform posts a draft to the member's Actions inbox. Nothing is signed or submitted at that point.

The draft appears as a "Request from an app". Its sheet says which app prepared it through the MCP server. Its checks record that, when the app prepared it, the member held the permission the change needs and the app filled in every required field. The member selects **Review** to open the item's sheet, then "Open draft", and the existing control opens with the values filled in. The member confirms with wallet verification. Opening the draft closes the item as accepted, whether or not the member then signs.

The app learns the draft's `itemId` and `openUrl` from the tool result. The link `/actions?agentItem=<itemId>` opens the item's sheet in the Actions inbox, and only the member it belongs to can open it.

### Refusals [#refusals]

A refused prepare call returns an error result, not a draft. The caller fixes the cause and calls again.

| Reason | What the platform observed | What to do |
| --- | --- | --- |
| `scope-missing` | The member did not allow the app to prepare writes. | Sign the app in again and allow **Prepare actions for you to review**. |
| `invalid-fields` | `assetId`, a field, `agentId` or `idempotencyKey` is missing or bad. | Fix the fields the message lists and call the tool again. |
| `asset-unreadable` | The member cannot read the asset. | Use an asset the member can see. |
| `permission-missing` | The member does not hold the permission the control needs on the asset. | Ask an administrator to grant the permission. Retrying does not help. |
| `no-agent-principal` | With `mandate`, `agentId` names no agent of the member's organisation. | Use an agent from the Agents page, or call the tool with `inbox`. |
| `execution-unsupported` | With `mandate`, the control has no call an agent can run. | Call the tool with `inbox`. |
| `mandate-refused` | The agent's mandate, its state or the organisation's halt refused it. | Read the reason in the message. Change the mandate or use `inbox`. |
| `restriction-held` | With `mandate`, the call asks to unblock a wallet, and an active [wallet restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet) on that wallet lists the asset. | Ask the organisation's owner to lift the restriction, as [Lift a restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet#lift-a-restriction) describes. A lift that completes unfreezes the wallet on the tokens the restriction froze. If the wallet was frozen on this asset before the restriction, call the unblock again once the restriction is lifted. Neither a retry nor `inbox` helps while the restriction is active. |
| `execution-unavailable` | DALP could not read the agent, the asset's metadata or the restrictions of the wallet to unblock, or could not reach the agent execution service. | Retry the call. |
| `store-unavailable` | DALP could not save the draft. | Retry the call. This is a retryable platform error. |

A `call_write_tool` call that names a tool the server does not list returns the error `Unknown tool: <name>`. List the tools again and call one of them.

### Execution modes [#execution-modes]

`execution` takes `inbox`, the default, or `mandate`. With `inbox` the draft goes to a person to sign. With `mandate`, DALP runs the write from an agent's account. An agent is a participant the organisation set up to act through a key the platform holds, and its [mandate](https://docs.settlemint.com/docs/concepts/custody-and-signing/agent-mandates) is the set of calls, amounts and expiry the chain enforces on it. The member's key never signs.

A `mandate` call names two more fields. `agentId` is the id of any of the organisation's agents from the Agents page: DALP checks that the agent belongs to the organisation, not that the member set it up. `idempotencyKey` is 8 to 128 letters, digits, dots, colons, dashes or underscores that the app sends again unchanged on a retry. A retry from the same app and member, with the same `agentId` and key, never runs the write twice. Another app, another member, another agent or an app that registered again as a new client starts a new write.

DALP makes the same checks as for a draft first, including the member's own permission for the control. Mint, burn, pause, resume, block and unblock run under a mandate. Any other control answers `execution-unsupported`. Every mint or burn row needs its holder and a positive amount with no more decimals than the asset has: DALP refuses an incomplete row or an over-precise amount instead of dropping or rounding it.

A [wallet restriction](https://docs.settlemint.com/docs/console/participants/restrict-a-wallet) freezes a participant's wallet on the tokens it lists, and DALP releases those freezes only through its lift. While one is active and lists the asset, DALP refuses an unblock of that wallet with `restriction-held`. The Console's "Unblock" sheet and the Platform API refuse the same unblock, so a draft prepared with `inbox` is refused before the member signs it.

A mandate meters transfers and approvals only, so a mandate that sets a limit on an asset cannot allow a mint or burn on it. DALP refuses such a mint or burn with `mandate-refused` and the reason `target-not-allowed`. When the mandate sets no limit on the asset and allows the call, the mint or burn runs with no amount cap. The answer carries the agent's account, the queue request and a `state` of `queued` or `landed`. A refused `mandate` call submits nothing and prepares no draft.

## Views in the app [#views-in-the-app]

In an app that renders MCP Apps, `show_holders` opens a table of an asset's holders and balances, as the member can read them. The tool takes `assetId`.

A prepared draft has no view in the app. The prepare result carries the draft's `openUrl` and tells the app to give that link to the member, who opens the item in the Actions inbox.

## Limits [#limits]

Tool calls share the assistant's limits. The per-member and per-organisation windows are the assistant's turn windows, multiplied by ten for tool calls, and they follow whether the organisation uses the shared assistant or its own model key. A call that spends the window is refused with 429 and a `Retry-After` header that gives the seconds until the window resets:

```json
{
  "error": "rate_limited",
  "error_description": "Too many MCP tool calls. Try again shortly."
}
```

The error is retryable. The app waits for `Retry-After` and sends the call again. A batch that carries several tool calls spends one unit per call.

## Other responses [#other-responses]

| Status | Meaning | What to do |
| --- | --- | --- |
| 401 | The token is missing (`invalid_request`), or it is expired or revoked (`invalid_token`). The `WWW-Authenticate` header points to the metadata. | Sign the app in again. |
| 403 | `mcp_server_off`: the organisation's MCP server is off, the assistant is off for the organisation or the whole platform, or the token carries no organisation. | Ask an owner to switch the server on. |
| 403 | `insufficient_scope`: the app was not granted `dalp:read`. Every tool needs it. | Sign the app in again and allow it to read. |
| 405 | A `GET` to `/api/mcp`. | Send JSON-RPC with `POST`. |
| 429 | The window is spent. | Wait for `Retry-After`. |
| 503 | DALP could not serve the request. This is a retryable platform error. | Retry after a moment. |

## Audit [#audit]

Every tool call writes one audit row before the call runs and settles it with the outcome. The row records the route `mcp.<tool>`, the auth source `oauth`, the member, the organisation, and the app's id and name next to the tool's arguments. A call made through `call_read_tool` or `call_write_tool` is recorded under the tool it runs. A prepare tool is recorded as a mutation, and any other tool as a sensitive read. DALP settles the rows of one request together: when the request fails, or any call in it fails or is refused, every row of that request is recorded as failed, including a call in the same batch that succeeded. A call refused for the limit window leaves no row, because it never ran. A `mandate` call also writes an `agent-execution` row with the auth source `agent`, the member as actor and the submitted calls. The rows are part of the [audit trail](https://docs.settlemint.com/docs/console/reporting/audit-trail).

## Connected apps and revocation [#connected-apps-and-revocation]

The **Connected apps** list on the Assistant page shows each connected app with these columns: "App", "Member", "Access", "Connected" and "Last used". "Access" reads "Read", "Prepare actions" or both. "Last used" reads "Never" until the app uses the grant. An empty list reads "No apps are connected."

An organisation owner sees every member's apps. A platform administrator who is not an owner sees only the apps they connected themselves.

Select "Revoke" on a row, then **Revoke access** to confirm:

| Who revokes | What the dialog says | What is removed |
| --- | --- | --- |
| An owner | The app loses access for every member who connected it. Members can connect it again. | The app's grants for the whole organisation. |
| A member | The app loses your access. You can connect it again. | Only that member's grant. |

A revoked app must be signed in again. A revoke that finds no matching grant answers as not found. The list and the revoke belong to the Console only. The published API reference, the SDK and the CLI do not offer them.

## Next steps [#next-steps]

- [CLI for AI agents](https://docs.settlemint.com/docs/api-reference/cli/ai-agents) covers the local `dalp --mcp` server, which the hosted server leaves unaffected, and how to scope an assistant session.
