---
title: "MCP server"
description: "Every operation in your API becomes a tool that coding agents can call. Zero dependencies, two transports, one login shared with your CLI, and a hosted endpoint at a stable URL."
url: https://typeship.dev/docs/platforms/mcp
markdown: https://typeship.dev/docs/platforms/mcp.md
section: "Get started"
---
> ## Documentation index
> Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt.
> Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page.

# MCP server

Every operation in your API becomes a tool that coding agents can call. Zero dependencies, two transports, one login shared with your CLI, and a hosted endpoint at a stable URL.

The `mcp` platform adds a Model Context Protocol server to your TypeScript package. It exposes each operation as a typed tool, ships a `search_docs` and `read_docs` pair so agents can read your reference and guides, and shares credentials with your CLI. It has no runtime dependencies. The bin is named after your API, so the Acme API ships `acme-mcp`.

The server speaks the current MCP revision, `2026-07-28`: stateless, one request at a time, no `initialize` handshake and no sessions. That is the shape the protocol settled on, and it is the shape a generated API server wants anyway. Claude Code connects to it out of the box; see [Protocol notes](#protocol-notes) for what other clients need.

The server reaches agents two ways: inside your package, as a process the client spawns or a Worker you deploy, or as the [hosted endpoint](#hosted-endpoint) typeship runs for you at a stable URL. To connect Claude Code, Cursor, or Claude Desktop, see [Connect MCP clients](https://typeship.dev/docs/guides/mcp-clients).

## Transports

The same compiled server speaks two transports. The transport decides where credentials come from.

### stdio

`node dist/mcp.js` speaks newline-delimited JSON-RPC 2.0 over stdin and stdout. This is the transport local MCP clients spawn themselves. Build the package once first:

```bash
cd acme
npm install
npm run build   # clients run dist/mcp.js
```

Credentials and the base URL resolve exactly as they do for the CLI. Environment variables win, then whatever `acme login` and `acme config` saved:

| Setting      | Environment variable                       | Saved by                      |
| ------------ | ------------------------------------------ | ----------------------------- |
| Bearer token | `ACME_TOKEN`                               | `acme login`                  |
| API key      | `ACME_API_KEY`, or one variable per header | `acme login`                  |
| Basic auth   | `ACME_USERNAME` / `ACME_PASSWORD`          | `acme login`                  |
| Base URL     | `ACME_BASE_URL`                            | `acme config set base-url`    |
| Environment  |                                            | `acme config set environment` |

One `login` covers the CLI and every agent client. No secrets need to live in client config files. If no base URL can be resolved, the first tool call returns an `isError` result naming the fix (`set ACME_BASE_URL` or `acme config set base-url`); the server stays up.

The package also declares the server as a bin, so `npx acme-mcp` (or `acme-mcp` after a global install) starts it the same way `node dist/mcp.js` does.

Nothing but JSON-RPC is written to stdout. Startup errors and `ACME_DEBUG=1` request lines go to stderr.

### Streamable HTTP

`node dist/mcp.js --http 3000` serves the same tools over HTTP. The port falls back to `PORT`, then 3000. The module also exports `handleHttp`, a fetch-style handler (`Request` in, `Response` out) you can mount on any route or edge runtime.

Requests without an `Origin` header (every non-browser MCP client) are always accepted. When `Origin` is present the server validates it, as the spec requires against DNS rebinding: same-host and localhost origins are allowed, anything else is `403` unless listed in `ACME_MCP_ALLOWED_ORIGINS` (comma-separated; `*` allows any). CORS headers reflect the allowed origin.

In HTTP mode, each request's `Authorization` header is forwarded to your API, so every caller uses their own credentials. Environment variables still work for credentials the server itself should hold.

The HTTP server is stateless, as the `2026-07-28` transport is by design. It accepts one JSON-RPC message per POST, never mints or reads session ids, and does not open a server-to-client stream. Notifications return `202`. Anything other than POST returns `405`.

Every request must carry the transport's mirrored headers, and the server checks them against the body as the spec requires: `MCP-Protocol-Version` (must equal the version in `_meta`), `Mcp-Method` (must equal `method`), and `Mcp-Name` on `tools/call` (must equal `params.name`; the `=?base64?…?=` encoding is decoded first). A missing or mismatched header is `400` with JSON-RPC error `-32020`. An unknown method is `404` with `-32601`. A JSON-RPC batch or a malformed message is `400` with `-32600`.

## The `acme mcp` command

The fastest way to wire the server into an agent client is the CLI's own `mcp` command:

```bash
acme mcp                    # print the server entry as JSON
acme mcp --claude           # write ./.mcp.json (Claude Code)
acme mcp --cursor           # write ./.cursor/mcp.json
acme mcp --claude-desktop   # write the Claude Desktop config
acme mcp --url https://typeship.dev/mcp/<slug> --claude   # a remote endpoint instead
```

Local entries are `{ "command": "node", "args": ["/abs/path/acme/dist/mcp.js"] }`. Remote entries are `{ "type": "http", "url": "..." }`. Existing servers in the target file are preserved. Only the `acme` entry is added or replaced. Claude Desktop's config file only launches local servers, so `--claude-desktop` with `--url` stops with the instruction to add the URL as a connector in the app instead.

The written entry has no `env` block. The server reads the credentials that `acme login` saved, so tokens never sit in a config file checked into a repo.

## Tools

One tool per operation, named `resource_method` in lower snake case: `accounts_list`, `accounts_get`, `spec_versions_get_content`. GraphQL root fields follow the same shape: `query_account`, `mutation_create_account`. Names are limited to lowercase letters, digits, and underscores, capped at 60 characters, and deduplicated.

* Descriptions give agents the operation summary, the first sentence of its description when that adds something, and the method and path as ground truth: `Create an account. Accounts hold a balance in one currency. POST /accounts`. Paginated tools add `(paginated: returns one page plus hasMore and nextPage arguments)`. The full description stays available through `read_docs`.
* Guardrails the spec already states are folded in, so the agent reads them before the call instead of in the error. A deprecated operation's description leads with `Deprecated.`; an operation whose `security` names OAuth scopes ends with `Requires scope accounts:write.`; an operation that works without credentials in an API that otherwise has them says `No credential needed.` Argument descriptions gain enum meanings from `x-enumDescriptions` (or `x-enum-descriptions` / `x-enum-varnames`) as `Values: active (open and usable), frozen (temporarily locked)`, the schema `default`, `Markdown; use literal newlines.` for `text/markdown` content, `Deprecated.` for deprecated parameters, and, for a `<thing>_id` argument whose thing has exactly one list operation, `IDs come from things_list.` The same notes appear in `read_docs` and CLI help.
* When the spec cannot say it (a three-step upload, a slow endpoint), write the tool description yourself: `mcp.tool_descriptions` in the project's [config](https://typeship.dev/docs/projects/config), keyed by operationId or `"METHOD /path"`, replaces the derived text for that operation in the package and on the hosted endpoint. Keys that match nothing are generation warnings.
* Each tool carries a `title` (the operation summary) and spec annotations: `readOnlyHint` for GET and HEAD, for GraphQL queries, and for POST operations the spec names as reads (`search`, `query`, `find`, `count`); `destructiveHint` for DELETE, PUT and PATCH (they remove or overwrite) and for operations named like `cancel`, `archive`, `revoke`, while POST creates are additive; `idempotentHint` for reads, PUT and DELETE; `openWorldHint` false, since the server talks to one known API. Clients such as Claude Code use these to auto-approve reads and confirm destructive writes.
* Input schemas are JSON Schema 2020-12 derived from your spec: real types, enums, formats, descriptions, defaults, and required fields for path, query, header, and body parameters. `allOf` compositions are merged into one flat argument list, `readOnly` properties are left out of inputs, and nullable fields keep their nullability. Nested schemas are inlined to a fixed depth so models can read them without resolving references.
* Tools carry an `outputSchema` when your spec documents a success body (paginated tools describe `items`, `hasMore`, `nextPage`), and successful results carry the JSON as `structuredContent` alongside the text block. Error results keep their JSON in the text only, since `structuredContent` must match the schema.
* Operations whose body is a plain object take its fields as top-level arguments. Other body shapes take a single `body` argument. Argument names are your wire names.
* Every tool also takes `fields`, an array of dotted paths that keeps only those keys of the result (`["id", "name", "owner.email"]`), applied per item on paginated tools. It is the same projection the CLI's `--fields` does, and the way to keep large responses small. An operation that already has a `fields` parameter keeps its own.
* GraphQL operations that return an object accept a `select` argument, a raw selection set such as `{ id name }`, that replaces the default depth-2 selection. It is declared in the tool's input schema with the default spelled out.

Every server also ships two documentation tools:

* `search_docs` searches your reference (operations, parameters) and, when a docs site is configured, your guides. Reference matches are ranked (tool name first, then summary, path and argument names, then description) and paged 15 at a time through `page`.
* `read_docs` returns an operation's full reference by tool name or dotted `resource.method`, or a guide page from your docs site.

The reference comes from the spec and is baked into the package. Guides come from your documentation site through the `llms.txt` convention. See [Docs for agents](#docs-for-agents).

### Tool mode for large APIs

Hundreds of per-operation tools flood an agent's context. The `meta` tool mode collapses the server to exactly three tools:

* `search_docs` and `read_docs`, as above.
* `execute`, which runs any operation by name with a JSON `arguments` object. It accepts tool names (`accounts_create`) or dotted names (`accounts.create`).

Set it with `mcp.tool_mode` in the project's [config](https://typeship.dev/docs/projects/config): `auto`, `operations`, or `meta`. `auto` is the default and switches to `meta` above 100 operations. The hosted endpoint honors the same setting.

### Arguments are checked, never dropped

Before anything reaches your API, the arguments of a tool call are checked against the tool's input schema, and every problem comes back at once as one `isError` result, so a single round trip fixes the call:

* An unknown argument name is an error with a suggestion (`"nam": did you mean "name"?`). When only case or punctuation differs (`accountId` for `account_id`), the value is matched to the accepted name and the call proceeds.
* Values are coerced where the intent is clear (`"true"` to a boolean, `"3"` to an integer, a JSON string to an object, a lone scalar to a one-item array, `US` to the enum member `us`) and rejected where it is not (`"abc"` for an integer), naming what was expected.
* Missing required arguments are listed with their descriptions.
* Date and date-time arguments take relative forms as well as ISO 8601: `-P7D` and `-7d` (seven days ago), `+PT1H` and `+2h` (ahead), `7 days ago`, `in 2 weeks`, `today`, `yesterday`, `now`. They resolve against the server's clock before the request, so your API sees an absolute value (`YYYY-MM-DD` for `date`, an ISO instant for `date-time`). An unsigned duration (`7d`) is rejected as ambiguous, naming the signed forms. The argument's description says which forms it takes.

```json
{
  "error": "InvalidArguments",
  "code": "INVALID_ARGUMENTS",
  "message": "2 problems with the arguments to accounts_create; nothing was sent to the API.",
  "issues": [
    { "code": "UNKNOWN_ARGUMENT", "argument": "nam", "message": "Unknown argument \"nam\"; did you mean \"name\"? Accepted: name, currency, tags, regions, metadata, fields." },
    { "code": "MISSING_ARGUMENT", "argument": "name", "message": "Missing required argument \"name\"." }
  ],
  "next_steps": ["Fix the arguments listed in issues and call accounts_create again.", "read_docs {\"page\": \"accounts_create\"} lists every argument with its type and whether it is required."]
}
```

Nothing is dropped silently: an argument your API would not understand never becomes a request that fails for an unclear reason, or worse, a request that succeeds without the model's intent.

## Results and errors

Tool results are compact JSON text content. Paginated tools return one page plus a continuation signal and, when there is another page, the exact arguments that fetch it, whatever the API's pagination style:

```json
{ "items": [ ... ], "hasMore": true, "nextPage": { "cursor": "cur_2", "limit": 20 } }
```

Results are capped at 64,000 characters (about 16k tokens, under every major client's own limit) so the agent sees an explanation instead of a mid-JSON cut. Over the cap, a page is cut to whole items and gains a `truncated` note saying how many were omitted and how to continue: offset and last-id pagination styles resume exactly at the cut through `nextPage`; cursor and page styles say to call again with a smaller page size. A plain object loses its largest keys, each replaced by a marker that names the `fields` call that fetches it alone. The package's server takes the cap from `<PREFIX>_MCP_MAX_RESULT_CHARS`.

API failures come back as tool results with `isError: true`, carrying the typed error, a stable code, your API's own error payload, and what to do next, so the agent can branch without parsing prose:

```json
{
  "error": "NotFoundError",
  "code": "NOT_FOUND",
  "message": "...",
  "status": 404,
  "body": { ... },
  "docs_url": "https://docs.acme.example",
  "next_steps": ["Check the id; list the resource first to find the right one."]
}
```

The codes are the generated CLI's: `NO_AUTH`, `AUTH_INVALID`, `PLAN_LIMIT`, `NOT_FOUND`, `INVALID_REQUEST`, `RATE_LIMITED`, `SERVER_ERROR`, `NETWORK_ERROR`, `VALIDATION_FAILED`, plus `INVALID_ARGUMENTS` for the checks above. A 401 names where credentials come from on the transport in use (the environment variable and `login` on stdio, the `Authorization` header over HTTP); a 429 carries the wait. See [Output and exit codes](https://typeship.dev/docs/platforms/cli#output-and-exit-codes) for what each code means. JSON-RPC protocol errors are reserved for unknown tools, unknown methods, and malformed requests.

### Files: binary responses and uploads

A binary response (an image, a PDF, an archive: anything the API returns that is not JSON or text) never comes back as `{}`. Images up to 4 MB come back as an `image` content block the model can look at, with a text summary (`media_type`, `bytes`) beside it. Other binaries depend on where the server runs:

* The package's server, run as a process on the agent's machine (stdio, or `--http` launched there), saves the bytes to a per-server directory under the OS temp dir and returns `{ saved_to, media_type, bytes }`, the same way the CLI's `--out` materializes files.
* A remote server (the [hosted endpoint](#hosted-endpoint), or the Cloudflare worker) has no disk of the agent's. Binaries up to 1 MB are embedded as a base64 `resource` block; larger ones return a description with the size and the advice to fetch through the SDK, the CLI, or the package's local server.

Uploads go the other way. Operations with a `multipart/form-data` body or a raw binary body are tools on the package's local server, where each file argument is a local path the server reads and sends (`"image": "/Users/me/avatar.png"`); an unreadable path is an argument error before anything is sent. On a remote server they are not tools, since the server cannot see the agent's files; the argument description and the server instructions say so. Generation reports which operations are affected.

## Read-only and narrower servers

Agents that should look but not touch get a server that cannot write. `--read-only` (or `<PREFIX>_MCP_READ_ONLY=1`) keeps only read operations: GET and HEAD, GraphQL queries, and POST operations the spec names as reads. Writes are out of the callable set, not merely unlisted: a write called by name is an unknown tool, and `execute` does not know it either. The server's instructions say it is read-only and how many operations that hides.

```bash
acme mcp install --claude --read-only     # registers node dist/mcp.js --read-only
```

`--tools accounts,transfers.create` (or `<PREFIX>_MCP_TOOLS`) narrows the server to named resources, tool names, or dotted `resource.method`, for agents that need a corner of a large API without the meta tool shape. Both switches compose with `mcp.tool_mode`. The [hosted endpoint](#hosted-endpoint) serves its read-only twin at `/mcp/<slug>/readonly`.

## Instructions for agents

`server/discover` returns instructions that agents read once when they connect: what the tools are, how arguments and results behave, where credentials come from on this transport, whether the server is read-only, and, when the API has an operation that returns the caller (the CLI's `whoami` target, or `cli.whoami_operation` in config), which tool to call first to learn whose credential this is. Add your own with `mcp.instructions` in the project's [config](https://typeship.dev/docs/projects/config) (up to 2,000 characters): what to call first, conventions the spec does not state, what not to do. The package's server and the hosted endpoint both carry it. `serverInfo` names your docs site as `websiteUrl` when one is configured.

Generation reports the size of `tools/list` as agents pay for it (characters and an approximate token count, in the generation's `meta`), and warns above 80 KB, since every client loads the list once per session.

## OAuth discovery

When your spec declares an OAuth2 flow, the HTTP transport serves RFC 9728 protected-resource metadata at `/.well-known/oauth-protected-resource`, naming the authorization server derived from the flow's authorization and token URLs. A request that arrives without an `Authorization` header, on a server holding no environment credentials, receives a `401` whose challenge points at that metadata:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.acme.example/.well-known/oauth-protected-resource"
```

That challenge is how MCP clients discover where to obtain a token. The OAuth flow itself runs against your authorization server. The MCP server never mints, stores, or validates tokens. It forwards the caller's bearer token to your API, which validates it like any other request.

## Docs for agents

Two layers back `search_docs` and `read_docs`, and the CLI's `docs` command:

1. **Reference.** Every operation's summary, description, and argument table, generated from your spec and shipped in the package. Works offline.
2. **Guides.** Your documentation site, read through `llms.txt` and `llms-full.txt`. Most docs hosts publish these automatically. The URL resolves in this order: `acme config set docs-url`, then `docs_url` in the project's [config](https://typeship.dev/docs/projects/config), then your spec's `externalDocs.url`.

Nothing is fetched unless a docs tool or command runs. Fetches time out after 10 seconds.

## Host it yourself

Every package generated with the `mcp` platform includes a Cloudflare Worker entry point (`src/worker.ts`) and a preconfigured `wrangler.toml`. Running a remote MCP server on your own account is one command:

```bash
npx wrangler deploy
```

Callers' `Authorization` headers pass through to your API, so the Worker holds no secrets by default. Use `npx wrangler secret put ACME_TOKEN` when the server itself should authenticate. A self-hosted deployment serves the spec it was generated from. The [hosted endpoint](#hosted-endpoint) is the always-current alternative that never needs redeploying.

## Hosted endpoint

_Available on Enterprise. typeship runs this service for you, priced by contract._

The server in your package is something your users install and run. The hosted endpoint is the same server, run by typeship, at a URL your users paste into their agent client:

```text
https://typeship.dev/mcp/<slug>
```

Turn on **hosted MCP endpoint** under the MCP server platform in [project settings](https://typeship.dev/docs/projects#platforms). It needs the MCP server platform on and Enterprise. The URL appears next to the switch, and on the API as `mcp_url`. Turning the MCP server platform off turns the endpoint off with it.

* Tools are built from the project's current spec with [spec patches](https://typeship.dev/docs/projects/spec-patches) applied. Changes are picked up within five minutes. Nothing to regenerate, nothing to redeploy.
* Tool calls run through the same request runtime every generated SDK ships: the same query and body encoding, retries with `Retry-After`, per-operation retry policies, idempotency keys, and page walking. A tool behaves the same on the hosted URL and in the package.
* Each caller's `Authorization` header passes through to your API. typeship holds no credentials for your API.
* Tool calls are rate limited per caller (by bearer credential, else by IP) at 120 per minute and per endpoint at 1,200 per minute; over the limit is `429` with `Retry-After`. `tools/list` and `server/discover` are not limited.
* Every tool call is counted per day and per tool. Project settings shows the last 30 days next to the endpoint URL: calls, errors, rate-limited calls, and average upstream latency.
* The same `search_docs` and `read_docs` tools ship, backed by your spec and your docs site.
* The project's `mcp.tool_mode` and `mcp.instructions` [config](https://typeship.dev/docs/projects/config) apply, so large APIs can serve the three-tool `meta` shape and agents read your guidance on connect.
* `/mcp/<slug>/readonly` is the same endpoint with every write left out of the callable set, for agents that should only read; it serves its own protected-resource metadata at `/mcp/<slug>/readonly/oauth-protected-resource`.
* Arguments are checked and coerced, results are projected with `fields` and capped in size, and errors carry codes and next steps, exactly as in the package.
* When your spec declares OAuth2, unauthenticated requests get the standard discovery challenge, with protected-resource metadata at `/mcp/<slug>/oauth-protected-resource`.

Any client that speaks Streamable HTTP can add the URL. Your CLI can write the entry for the common ones:

```bash
acme mcp --url https://typeship.dev/mcp/<slug> --claude
acme mcp --url https://typeship.dev/mcp/<slug> --cursor
```

See [Connect MCP clients](https://typeship.dev/docs/guides/mcp-clients) for the manual configuration.

### Hosted or self-hosted

|             | Hosted endpoint                            | Your package's server                            |
| ----------- | ------------------------------------------ | ------------------------------------------------ |
| Runs on     | typeship                                   | Your users' machines, or your Cloudflare account |
| Spec        | Always the project's current spec          | Frozen at generation                             |
| Credentials | Passed through per request                 | Environment, or saved by `acme login`            |
| Rate limit  | 120/min per caller, 1,200/min per endpoint | None built in                                    |
| Usage       | Counted, shown in project settings         | Not collected                                    |
| Setup       | Paste a URL                                | Build the package, or `npx wrangler deploy`      |

Both share tool names, input schemas, argument checking, result shaping, the error contract, and the docs tools. Point external users at the hosted URL, and keep the package's server for local development and for users who need the server inside their own network. Upstream requests from the hosted endpoint time out after 30 seconds.

## Protocol notes

* Protocol revision `2026-07-28` only. Every request carries `params._meta["io.modelcontextprotocol/protocolVersion"]` and `["io.modelcontextprotocol/clientCapabilities"]`; a request without them is `-32602`, a request for another version is `-32022` with the supported list in `error.data`. Every result carries `resultType: "complete"` and the server's identity in `_meta`. `server/discover` answers with the supported versions, the `tools` capability, and short instructions.
* `tools/list` results carry `ttlMs` (one hour: the tool set is fixed at generation) and `cacheScope: "public"`, so clients and shared caches may hold them. Tools come back in spec order, so the list is stable across calls.
* Cancellation: on stdio, `notifications/cancelled` for an in-flight request suppresses its response; on HTTP, closing the connection is the signal. Other notifications are accepted and ignored.
* No legacy handshake. A client that opens with `initialize` (protocol revisions `2025-11-25` and earlier) gets an error that names `2026-07-28`, as the spec asks of modern-only servers. Claude Code and Claude Desktop speak `2026-07-28` today; Cursor (3.2) does not yet. Clients built on the official SDKs speak it when they enable version negotiation (TypeScript: `versionNegotiation: { mode: "auto" }` or a pin); with the SDK's default settings they still open with `initialize` and will not connect. Ask your API's consumers to update their client if that happens; the error message tells them why.
* The server exits when its stdin closes.
* On stdio, one JSON-RPC message per line on stdin and stdout.
* Operations that stream server-sent events are not exposed as tools (a tool result is one value, not a stream); they are still SDK methods and CLI commands, and generation reports which ones. Operations with binary or multipart request bodies are tools on the package's local server only; see [Files](#files-binary-responses-and-uploads).

## Sitemap

[Every page of these docs](https://typeship.dev/llms.txt)
