# MCP server

fuast has a remote [Model Context Protocol](https://modelcontextprotocol.io) server at:

```txt
https://fuast.com/mcp
```

An agent that connects to it can read the docs, start from a template, check an app, deploy it, test it, read its logs and data, and share it. The agent needs no terminal and no file system: it sends and receives file contents. Agents that do have a terminal can keep using the CLI. See the [agent start guide](https://fuast.com/docs/agents.md).

On a self-hosted platform the URL is `<FUAST_BASE_URL>/mcp`.

## Connect your agent

The server URL is the same for every client. The first time a tool needs your account, the client opens a fuast sign-in page in your browser. [fuast.com/agents](https://fuast.com/agents) has a page per client, with one-click links for Claude, Cursor, and VS Code.

### One command for coding agents

```sh
curl -fsSL fuast.com/install | sh
```

The script connects this server to Claude Code, Codex, and Gemini CLI, whichever are installed, and saves the [fuast skill](#the-fuast-skill) where each one reads skills. It creates no account and deploys nothing, and you can [read it](https://fuast.com/install) first. Run it again to update the skill.

### Claude (web, desktop, mobile)

1. Open **Customize > Connectors** and choose **Add custom connector**, or use the link on [fuast.com/agents/claude](https://fuast.com/agents/claude), which fills in the dialog.
2. Enter `https://fuast.com/mcp`.
3. Ask Claude to build something. When it reaches a tool that needs your account, choose **Connect** and approve.

### Claude Code

```sh
claude mcp add --transport http --scope user fuast https://fuast.com/mcp
```

`--scope user` makes the server available in every folder. Without it, Claude Code adds the server for the current folder only. Run `/mcp` inside Claude Code to sign in. To use a token instead of the browser sign-in:

```sh
claude mcp add --transport http --scope user fuast https://fuast.com/mcp \
  --header "Authorization: Bearer $FUAST_TOKEN"
```

### Codex

```sh
codex mcp add fuast --url https://fuast.com/mcp
```

To sign in, run `codex mcp login fuast`. It opens your browser.

### Gemini CLI

```sh
gemini mcp add --transport http --scope user fuast https://fuast.com/mcp
```

To sign in, run `/mcp auth fuast` inside Gemini CLI.

### Cursor

Use the **Add to Cursor** link on [fuast.com/agents/cursor](https://fuast.com/agents/cursor), or add this to `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project:

```json
{
  "mcpServers": {
    "fuast": { "url": "https://fuast.com/mcp" }
  }
}
```

With a token instead of the browser sign-in:

```json
{
  "mcpServers": {
    "fuast": {
      "url": "https://fuast.com/mcp",
      "headers": { "Authorization": "Bearer ${env:FUAST_TOKEN}" }
    }
  }
}
```

### VS Code

Use the **Add to VS Code** link on [fuast.com/agents/vscode](https://fuast.com/agents/vscode), or add this to `.vscode/mcp.json`:

```json
{
  "servers": {
    "fuast": { "type": "http", "url": "https://fuast.com/mcp" }
  }
}
```

### ChatGPT

1. Open **Plugins**, choose **+**, then **Add custom MCP server**.
2. Enter `https://fuast.com/mcp` and pick OAuth as the authentication.
3. Create it, then sign in to fuast when ChatGPT asks.

### Any other client

Use Streamable HTTP with the URL above. The client needs to support OAuth for remote MCP servers, or let you set an `Authorization` header.

The server is described for the [MCP registry](https://registry.modelcontextprotocol.io) at [fuast.com/server.json](https://fuast.com/server.json).

## The fuast skill

A skill is a short instruction file a coding agent loads when a request matches it. The fuast skill tells the agent when fuast fits ("make a little app for my team"), how to build and deploy with the CLI or with this server, and the rules agents most often get wrong. Read it at [fuast.com/skill.md](https://fuast.com/skill.md).

The [install script](#one-command-for-coding-agents) saves it for Claude Code, Codex, and Gemini CLI. For Cursor, GitHub Copilot, and other agents that read skills:

```sh
npx skills add https://fuast.com
```

To add it by hand, save the file as `fuast-build-and-host-small-apps/SKILL.md` in the agent's skills folder, for example `~/.claude/skills/fuast-build-and-host-small-apps/SKILL.md` for Claude Code. The long name is on purpose: an agent with many skills installed often sees only their names, so the name has to say what the skill is for.

A capsule made with `fuast new` or `fuast init` also carries an `AGENTS.md` with the rules for that version of fuast. The skill gets the agent to fuast; `AGENTS.md` is what it follows once it is in the project.

## Three ways to authenticate

The server reads who is calling from the `Authorization: Bearer` header only. It never reads cookies.

### No account

A connection without credentials can still ship something:

- `fuast_guide`, `fuast_docs`, `list_templates`, `get_template`: read the docs and the starter apps.
- `check_app`: build and type check files without deploying.
- `deploy_app`: deploy a temporary preview. It returns `preview_url`, `claim_url`, `edit_token`, and `expires_at`.

Give the claim link to the person the app is for. They open it, sign in to fuast, and the app becomes theirs. The agent keeps the edit token and passes it as `edit_token` to `update_app_files`, `get_app_source`, `get_app`, `get_logs`, `inspect_app`, `call_app`, and `get_claim_link` to keep working on that preview.

A preview is not a full app. It is deleted at `expires_at` (72 hours after it was created, by default) unless someone claims it. It has no accounts, so every visitor is a guest. Its schedules and jobs wait until it is claimed. Its upload and data limits are smaller: the deploy result lists them under `limits`, and [limits](https://fuast.com/docs/limits.md#unclaimed-previews) explains them. The edit token is a secret: keep it in the conversation, never in the app's files. After a claim it stops working, and the app is reached through the owner's account.

Previews exist only on platforms that serve each app on its own subdomain, which fuast.com does.

### Sign in with your fuast account (OAuth)

Tools that work on your apps need your account. The first call to one of them answers HTTP `401` with a `WWW-Authenticate` header, and the client starts sign-in: you sign in to fuast, read a consent screen that names the app, where it sends you back to, your workspace, and what it may do, and approve. The client then repeats the call. To sign in before deploying, ask the agent to call `whoami`.

A connection gets only the scopes you approved:

| Scope | Lets the agent |
| --- | --- |
| `apps:read` | List your apps and read their source, logs, data, and usage. Run queries with `call_app`. |
| `apps:write` | Deploy, roll back, and delete apps, set their env vars, and run mutations and actions with `call_app`. |
| `apps:share` | Change who can open an app and invite people by email. |

The challenge names the scopes the call needs, so a client that follows it asks for the least. A request without any scope gets `apps:read`. When a later call needs a scope the connection lacks, the server answers `403` with `error="insufficient_scope"` and the full list to ask for, and the client asks you to approve the larger set.

A signed-in connection acts as you, with your permissions, and only through these tools. Its token is not accepted by the fuast API or by any app.

### API token

Clients that take a static header can send a fuast API token:

```sh
fuast token create --name my-agent          # personal: everything you can do
fuast token create --name ci --app my-app   # app deploy token: one app only
```

Send it as `Authorization: Bearer <token>`. A personal token can use every tool. An app deploy token can deploy, roll back, and read the status of its own app, and nothing else: it sees a shorter tool list, `deploy_app` and `update_app_files` must name its app, and `inspect_app` gives it `manifest`, `usage`, and `schedules` only. The session that `fuast login` saves is not accepted here. See [account and tokens](https://fuast.com/docs/reference.md#account-and-tokens).

Deploying into an account needs a verified email address. Until then deploys answer `Verify your email address first. Check your inbox for the link.`

## Tools

Every tool returns text and the same data as `structuredContent`, with an `outputSchema`. Arguments are checked against the tool's `inputSchema`, and unknown arguments are refused. A failure the agent can fix comes back as a tool result with `isError: true` and a message that says what to do.

"Account" is what the tool needs when it is called with `app`. Tools that also take `edit_token` work on a preview without an account.

| Tool | What it does | Account | Kind |
| --- | --- | --- | --- |
| `fuast_guide` | The guide for connected agents and the capsule rules. Call it first. | No | Read-only |
| `fuast_docs` | Read one docs page (`page`), search every page (`search`), or list the pages. | No | Read-only |
| `list_templates` | The starter apps and what each one shows. | No | Read-only |
| `get_template` | One template's files as `{path: content}`, named with `name`. | No | Read-only |
| `check_app` | Build, type check, and load `files` in a sandbox without deploying. Returns the handlers and tables the app declares, or diagnostics with file, line, and message. | No | Read-only |
| `deploy_app` | Deploy `files`. Creates a private app in your workspace, updates `app`, or (without an account) deploys a preview. Updates a preview with `edit_token`. | `apps:write`, or none for a preview | Destructive |
| `get_app_source` | The live deploy's source as `{path: content}` and its `deploy_id`. Apps over 400 KiB return the file list: ask again with `paths`. Takes `app` or `edit_token`. | `apps:read` | Read-only |
| `update_app_files` | Redeploy with some files changed: `files` maps a path to new content, or to `null` to delete it. `expected_deploy_id` must be the live deploy. Takes `app` or `edit_token`. | `apps:write` | Destructive |
| `list_apps` | The apps you can use: yours, your workspace's, and apps shared with you. | `apps:read` | Read-only |
| `get_app` | One app: URL, status, sharing, handlers, the last 10 deploys, today's usage, env var names. Takes `app` or `edit_token`. | `apps:read` | Read-only |
| `get_logs` | Recent log entries, oldest first, with a `cursor` for the next call. `level`, `since`, `limit` (at most 200). Takes `app` or `edit_token`. | `apps:read` | Read-only |
| `inspect_app` | `what`: `manifest`, `tables`, `rows` (one `table`, at most 100 rows per call), `usage`, `schedules`, or `jobs`. Takes `app` or `edit_token`. | `apps:read` | Read-only |
| `call_app` | Run one query, mutation, or action and return its result and the log lines it wrote. Takes `app` or `edit_token`. | `apps:read` for a query, `apps:write` for a mutation or action | Destructive |
| `share_app` | Set `visibility` (`private`, `workspace`, `public`), `invite` people by email, `remove` people. | `apps:share` | Destructive |
| `set_env` | `set` and `unset` env vars of an app. Values are write-only. | `apps:write` | Destructive |
| `rollback_app` | Make an earlier deploy live again (`deploy_id`, or the previous ready deploy). | `apps:write` | Destructive |
| `delete_app` | Delete an app and everything in it. Owner only, and `confirm` must repeat the slug. | `apps:write` | Destructive |
| `get_claim_link` | The claim link, preview link, and expiry of a preview, from its `edit_token`. | No | Read-only |
| `whoami` | The connected account, workspace, role, and scopes. Starts sign-in when nothing is connected. | `apps:read` | Read-only |

Who may do what is the same as in the dashboard and the CLI:

- Opening an app follows its [sharing](https://fuast.com/docs/teams.md#sharing). `list_apps`, `get_app`, and `get_app_source` work on any app you can open.
- Deploying, logs, inspection, `call_app`, `share_app`, `set_env`, and `rollback_app` need the app's owner or a workspace admin.
- `delete_app` needs the owner.
- An app you cannot see answers exactly like an app that does not exist.

Notes on single tools:

- **`deploy_app` and `update_app_files`** run the normal deploy pipeline: the same checks, the same sandboxed build, and a type check that cannot be skipped. A failed build deploys nothing. If a schema change would delete a table that holds rows, the error names the tables and the `delete_tables` argument that confirms it. A redeploy of an app in your account keeps the env values the previous deploy carried. A redeploy of a preview by its edit token starts with no env.
- **`call_app`** runs as the connected person (`as: "me"`, the default): the handler sees your identity in `ctx.auth`, exactly as if you had opened the app. It never runs as another user. `as: "guest"` runs it as a guest visitor, and works only on public apps that do not require sign-in. On a preview every call runs as one guest. Calls count against the app's daily quota, and mutations and actions change real data.
- **Restricted apps.** An app that was claimed from a preview is [restricted](https://fuast.com/docs/security.md#restricted-apps) until its owner trusts it: email, team tables, the member list, schedules, and jobs are off. `get_app` and `list_apps` report it as `restricted: true`. `deploy_app` and `update_app_files` work on it like on any app you may manage, and they never lift the restriction: their result has `restricted: true`, a `restricted_hint` that says workspace features are off, and `trust_command` and `trust_url` for the person. No tool trusts an app. The owner or a workspace admin does that outside MCP, with `fuast trust <slug>` or "Trust this app with my workspace" on the app's page in the dashboard. A preview's edit token stops working at the claim.
- **`get_app_source`** never returns `.env` files, and no tool returns env values.
- **`inspect_app`** returns the same manifest and usage documents as `fuast inspect`. Rows are capped per call and open to the owner and workspace admins only, like the dashboard's Data tab.

## Guide for connected agents

You are connected to fuast through MCP. You have no terminal and no file system here: you build an app by sending its files to these tools. A fuast app (a capsule) is a small full-stack TypeScript app with `server/index.ts`, `client/index.tsx`, and optional `shared/` files. It uses no npm packages.

Work in this order:

1. Start from a template. `list_templates`, then `get_template({id, name})`, returns a complete working app as `{path: content}`. Keep that file map and edit it. You do not need `tsconfig.json` or type declaration files.
2. Read what you need. The rules below cover the essentials. `fuast_docs({page: "capsule-api"})` has every API, and `fuast_docs({search: "..."})` finds a topic.
3. Call `check_app({files})`. It builds and type checks without deploying. Fix every diagnostic (each has a file, a line, and a message) and check again until `ok` is true.
4. Call `deploy_app({files, name})`. It returns the URL and a `deploy_id`. Send every file.
5. Verify the deployed app. `call_app` runs a query, mutation, or action and returns its result and logs. `get_logs` shows handler errors. `inspect_app` with `what: "tables"` or `"rows"` shows the data. Read a write back instead of assuming it happened.
6. For a later change, call `update_app_files({app, files, expected_deploy_id})` with only the files that change. A `null` value deletes a file. If it answers that the app changed, call `get_app_source` and apply your change to what it returns.
7. Tell the person the URL, who can open the app, and what you checked.

Accounts:

- Without a connected account, `deploy_app` deploys a temporary preview and returns `preview_url`, `claim_url`, `edit_token`, and `expires_at`. Give the person the claim link: they open it, sign in, and the app becomes theirs. Say when the preview is deleted if nobody claims it.
- Keep `edit_token` in this conversation and pass it instead of `app` to `update_app_files`, `get_app_source`, `get_app`, `get_logs`, `inspect_app`, `call_app`, and `get_claim_link`. Never write it into the app's files. Once the preview is claimed the token stops working: use `app` with the slug.
- A preview has guests only, and its schedules and jobs wait until it is claimed.
- A claimed preview is restricted (`restricted: true` in `get_app` and in every deploy result): email, team tables, the member list, schedules, and jobs stay off until the owner trusts the app. You can keep deploying to it with `deploy_app` and `update_app_files`; that does not turn them on, and no tool does. If the app needs them, tell the person to press "Trust this app with my workspace" on the app's page (`trust_url`) or to run `fuast trust <slug>`.
- If the person has a fuast account, or wants the app in their own workspace, call `whoami`. The first call asks them to sign in. After that `deploy_app` creates private apps in their workspace.

Rules for these tools:

- New apps are private. Call `share_app` only when the person asks, and pick the narrowest mode that works.
- Set secrets with `set_env({app, set: {NAME: "value"}})` and read them as `ctx.env.NAME` in handlers. It works on apps in an account, not on previews. Never put a secret in `client/`, `shared/`, a tool description, or a log message.
- `call_app` runs as the connected person. Mutations and actions change real data.
- Call `delete_app`, and pass `delete_tables`, only when the person explicitly asks for data to be deleted.
- The sections below were written for the fuast CLI. Here the tools replace it: `check_app` is `fuast check`, `deploy_app` is `fuast deploy`, `share_app` is `fuast share`, `call_app` is `fuast run`, `get_logs` is `fuast logs`, and `inspect_app` is `fuast inspect` and `fuast db dump`. There is no local dev server, no `fuast login`, and no `.env.fuast.server` file: you check with `check_app` and test against the deployed app.

## Limits

| Limit | Value |
| --- | --- |
| Requests without an account | 60 per minute per client address |
| Requests with an account | 600 per minute per account |
| `check_app`, `deploy_app`, `update_app_files` | 30 per minute per client address |
| `check_app` without an account | 12 per minute per client address, 4 running at once across the platform |
| Builds running at once (`check_app`, `deploy_app`, `update_app_files`) | 3 per account, 2 per client address without an account |
| `call_app` | 600 per minute per client address, plus the app's [daily quotas](https://fuast.com/docs/limits.md) |
| Requests with a wrong token | 30 per 10 minutes per client address, then `429` |
| Request body, calls without a file map | 256 KiB |
| File map, signed in | The deploy limits: 1000 files, 20 MiB in total, 1 MiB per file |
| File map, no account | The smaller preview limits. The error states them. |
| `get_app_source` without `paths` | 400 KiB of source. Larger apps return the file list only. |
| `call_app` result | 200 KiB of JSON. Larger results are cut and marked `truncated`. |

A client address is an IPv4 address, or a whole IPv6 `/64`. Over one of the first two limits the server answers `429` with `Retry-After`, and a platform operator can change them. Over a per-tool limit the tool returns an error that says to wait. See [limits](https://fuast.com/docs/limits.md) for what an app itself may use.

## Protocol

- **Transport.** Streamable HTTP on one endpoint. Send each JSON-RPC message as its own `POST` with `Content-Type: application/json`. `GET` and `DELETE` answer `405`: there is no server-to-client stream and no session to end. A JSON-RPC batch (an array) answers `400` and nothing in it runs. That includes revision `2025-03-26`, the one revision that allowed batches.
- **Revisions.** `2026-07-28` (stateless: the version, client info, and capabilities travel in every request's `_meta` and in the `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` headers), and `2025-11-25`, `2025-06-18`, and `2025-03-26` through the `initialize` handshake. An unsupported version answers `400` with the list of supported ones.
- **No sessions.** The server never sends `Mcp-Session-Id` and remembers nothing between requests.
- **Methods.** `initialize`, `ping`, `server/discover`, `tools/list`, `tools/call`. `subscriptions/listen` is acknowledged and closed at once, because the tool list does not change while a client listens. There are no resources and no prompts.
- **Responses.** A single JSON object. A build can take a while, so `check_app`, `deploy_app`, and `update_app_files` switch to a `text/event-stream` response after 5 seconds when the client's `Accept` header allows it. The stream carries keep-alive comments, then the one JSON-RPC response, and ends.
- **Sign-in signals.** A call that needs an account answers `401` with `WWW-Authenticate: Bearer error="invalid_token", resource_metadata="...", scope="..."`. A call that needs a scope the connection lacks answers `403` with `error="insufficient_scope"`. Both bodies are also a tool result with the challenge under `_meta["mcp/www_authenticate"]`. Each entry of `tools/list` carries `_meta.securitySchemes` (`noauth`, `oauth2` with scopes).
- **Browsers.** A request with an `Origin` header from another website is refused with `403`. See [security](https://fuast.com/docs/security.md#mcp-and-oauth).

## OAuth for client builders

| | |
| --- | --- |
| Protected resource metadata | `https://fuast.com/.well-known/oauth-protected-resource/mcp` and `https://fuast.com/.well-known/oauth-protected-resource` |
| Authorization server metadata | `https://fuast.com/.well-known/oauth-authorization-server` |
| Issuer | `https://fuast.com` |
| Resource | `https://fuast.com/mcp` |
| Endpoints | `/oauth/authorize`, `/oauth/token`, `/oauth/register`, `/oauth/revoke` |

- **Grants.** Authorization code and refresh token.
- **Public clients only.** There are no client secrets. `token_endpoint_auth_methods_supported` is `["none"]`.
- **PKCE is required**, with `code_challenge_method=S256`.
- **Client identity.** Either a Client ID Metadata Document (the `client_id` is an `https` URL with a path, and the document must name the same `client_id` and allow the `none` auth method), or dynamic client registration at `POST /oauth/register` (JSON, `redirect_uris` required, `token_endpoint_auth_method` `none`).
- **Redirect URIs.** `https`, or `http` on `localhost`, `127.0.0.1`, or `[::1]`. They are matched exactly. A loopback URI matches with any port, so a native client can bind a free one. No wildcards, fragments, or credentials. A redirect URI on fuast's own addresses (`fuast.com`, any `*.fuast.com`, a custom domain of a fuast app) is refused: use your own site.
- **Errors before consent are shown to the person, not redirected.** A request with a missing `code_challenge`, a wrong `response_type`, or a foreign `resource` gets a fuast error page that names the OAuth error code. Your redirect URI is only ever called with a `code`, or with `error=access_denied` when the person chose Cancel.
- **`resource`.** When sent, it must be the MCP URL above. Tokens are issued for that resource and for nothing else.
- **`state`** is echoed. Every authorization response, errors included, carries `iss` (RFC 9207).
- **Authorization codes** work once and for 60 seconds, and only with the same client, redirect URI, and PKCE verifier.
- **Access tokens** last 1 hour. **Refresh tokens** last 30 days and rotate: each use returns a new one and ends the old one. Using an old one again ends the whole connection, and the token endpoint answers `invalid_grant`.
- **`POST /oauth/token` and `/oauth/revoke`** take `application/x-www-form-urlencoded`. Revoking either token of a connection ends the connection.
- **Consent** is asked on every authorization. There is no silent approval. The screen shows your redirect host as a checked fact and your `client_name` as a name you chose. A client whose metadata document is on a well-known host is shown as verified by that host.

## Disconnect an agent

Open **API tokens** in the dashboard and find **Connected agents**, or open [/dashboard/connect](https://fuast.com/dashboard/connect). Each connection shows the app's name, where it sends you back to, its scopes, and when it was last used. **Disconnect** ends it at once: its access and refresh tokens stop working. Removing the connector in the agent's own settings also works when the client tells fuast.

API tokens are revoked on the same page, or with `fuast token revoke <id>`.

Resetting your fuast password disconnects every agent and revokes every API token at once. Do that if you think a connection or a token leaked, then connect again.

What agents did through MCP is recorded: every deploy, share, env change, rollback, delete, and mutation or action run, plus each connect and disconnect. Deploys and other changes also show in the app's logs with the client's name.

## Troubleshooting

- **The client cannot reach the server.** Check the URL: `https://fuast.com/mcp`, on the dashboard's host. An app's address such as `https://my-app.fuast.com/mcp` is the app, not this server. A `404` on the right host means the platform has the MCP server turned off.
- **A tool answers "Sign in to fuast to use ..." and no sign-in page opens.** The client does not act on the `401` challenge. Connect with an API token header instead.
- **`401` after it worked before.** The connection was revoked, the account's password was reset, or the refresh token ran out after 30 days without use. Connect again.
- **"needs the apps:write permission".** The connection was approved with fewer scopes. Approve the larger set when the client asks, or remove the connector and add it again.
- **"Verify your email address first."** Open the link fuast mailed you, then deploy again.
- **"changed since you read it".** Someone deployed between your read and your update. Call `get_app_source` and apply the change to those files.
- **"This edit token does not open a preview".** The preview expired, was deleted, or was claimed. Deploy a new preview, or sign in as the owner and use `app`.
- **"Unsupported protocol version".** The error's `data.supported` lists the revisions this server speaks.
- **`403` from a web page.** Browser pages on other sites cannot call the server. Use an MCP client.
- **A `fuast login` token does not work as a header.** Create an API token with `fuast token create`.
