> For the complete documentation index, see [llms.txt](/llms.txt)

# The MCP server

From the same [Glotto IR](/docs/glotto-ir), Glotto generates a **multi-mode MCP server** your
customers ship, so AI clients (Claude Desktop, Claude Code, Cursor, …) can call your API.
It's a TypeScript server built on the official MCP TypeScript SDK v2
(`@modelcontextprotocol/server`), speaking the **2026-07-28 MCP spec** — the stateless core
(no protocol-level sessions, serverless/edge-friendly) with `server/discover`, per-request
version negotiation, and the required standard request headers handled by the SDK.

## Three modes, one artifact

Selected at server start (`--mode tools|code|dynamic`, `GLOTTO_MCP_MODE`) or per request on the
HTTP transport (`?mode=`) — no regeneration:

- **Tools Mode** (the default) — one MCP tool per API operation (`<resource>_<method>`). Best
  for small, focused APIs.
- **Code Mode** — exactly two tools, `execute` (runs TypeScript in a sandboxed isolate against
  your generated SDK, pre-authenticated — see *Transports & auth* below) + `search_docs`.
  Drastically reduces context use and supports chained calls — the competitive wedge for large
  APIs on code-capable agents.
- **Dynamic Mode** — three meta-tools over the same operations: `list_tools` (a compact,
  schema-free menu, optionally narrowed by resource), `describe_tools` (full JSON-Schema input
  schemas on demand — the same fidelity Tools Mode registers), and `invoke_tool` (validates the
  arguments, then executes the identical request and result rendering as the per-operation
  tool, including its `jq_filter` projection). The honest fallback for **large APIs on agents
  that cannot execute code** — schemas arrive on demand instead of up front, so context cost
  stays near-constant however many operations your API has.

`glotto.yml#/mcp/modes` restricts which modes the server enables (omit it to enable all
three); a requested-but-disabled mode falls back to the enabled default.

`search_docs` is **always available** in every mode, even when other tools are filtered out.

### What a `search_docs` hit tells you

A reference hit describes the operation, not just its name. Alongside the tool name and the
`client.<resource>.<method>` SDK call, each hit carries:

- **`requestBody`** — the body's type, its content type, and whether it is required.
- **`response`** — the type of the first success response.
- **`parameters[]`** — each parameter's location, whether it is required, its description from your
  spec, and a full `schema` describing it.

A `schema` keeps what a caller needs in order to construct a value: enum members (and any
deprecation notice on a member), string and integer `format`, array item types, map value types,
object fields and which of them are required, union members and any discriminator, and nullability.
A named model appears as `{ "kind": "model", "model": "Pet" }` rather than being expanded inline.

Those names resolve through the payload's `models` map, which carries the models the returned hits
reference — including ones reached only through other models:

```json
{
  "results": [
    {
      "kind": "reference",
      "tool": "pets_create",
      "sdkCall": "client.pets.create",
      "requestBody": { "required": true, "contentType": "application/json",
                       "schema": { "kind": "model", "model": "Pet" } },
      "response": { "kind": "model", "model": "Pet" }
    }
  ],
  "models": {
    "Pet": { "kind": "object", "required": ["name"],
             "fields": { "name": { "kind": "string" },
                         "status": { "kind": "enum", "values": ["available", "sold"] } } }
  }
}
```

The payload is this `{ results, models }` object rather than a bare array of results. If you consume
`search_docs` output directly, read `.results` from it; each entry keeps every field it had before.

### Browsing the surface without a query

Searching only helps once you can guess an API's vocabulary, and Code Mode has no `tools/list` to
fall back on. So **calling `search_docs` with no `query` browses the operations instead of searching
the docs**: `results` becomes a compact listing — tool name, one-line title, resource, method, HTTP
method, path — alongside a `resources` table of contents counting the operations in each. Narrow it
with `resource`, page it by echoing back the `nextCursor` a response returns, and search a listed
tool name to get that operation's full record. Pages are bounded in size, so browsing a large API
never dumps its whole surface into one response.

The listing shows what the server can actually invoke: in Tools and Dynamic Mode that is exactly
what `list_tools` reports, filters included; in Code Mode — which registers no per-operation tools
for a filter to hide — it is everything `glotto.yml#/mcp/permissions` allows.

`resource` and `cursor` shape a browse, so passing either alongside a `query` is an error rather
than a silently ignored argument.

A **search** returns the ten best matches plus `total`, the number of records that actually matched.
Without that count a query matching your whole API and one matching exactly ten results look
identical, so `total` is what tells you an answer was truncated and the query is worth narrowing.

### `execute` type-checks before it runs

An agent's first attempt at an unfamiliar API is often wrong, and the cheapest place to find that
out is *not* your production upstream. So before `execute` runs anything, it type-checks the
submitted code against the SDK it is about to bind. Code that calls a method the SDK does not have,
or passes a wrongly-shaped request body, is **not run**: nothing is sent to your API, and the
diagnostics come back as the tool result — at line and column numbers in the code the agent
submitted, so it can correct the call and resubmit.

That matters most for the mistakes that would otherwise *succeed at reaching you*. A hallucinated
method is only a wasted round-trip, but a wrong request body is a real, credentialed request
against your API — one that can mutate state on a `POST` and answers with your validation error
rather than "that field does not exist".

This is a correctness aid, not an access control. A `// @ts-ignore` suppresses it, so what a run is
*permitted* to reach is still governed by the sandbox permissions and by
[method permissions](#method-permissions). Set `GLOTTO_MCP_CODE_MODE_TYPECHECK=off` to skip the
check; it is on by default.

### Your custom code is not bound in Code Mode

Every generated TypeScript SDK re-exports your never-overwritten `lib/` directory as `lib`, so
`sdk.lib.myHelper()` is part of your SDK's public surface. A Code Mode run does **not** carry it: the
binding hands the isolate the generated SDK, and your own `lib/` sources are not part of it.

Rather than let that surface as `undefined`, an agent that reaches for one of your helpers is stopped
twice — the type-check above refuses the call before the run starts, and if the check is off or
suppressed, the access throws an error that names the boundary instead of
`undefined is not a function`. Only *reaching for a member* fails: code that logs, serializes, or
passes `lib` around keeps working.

Call the generated client from `execute` and keep helper logic on the agent's side of the sandbox.

### Code Mode's one prerequisite: Deno

`execute` runs the agent's code in a [Deno](https://deno.com) sandbox, so a **self-hosted** Code
Mode server needs the Deno CLI on its host (`GLOTTO_MCP_DENO_PATH` selects the binary when it
isn't on `PATH`). Two paths need no install at all: the emitted **Docker image already includes
Deno**, and the hosted Glotto MCP Cloud gateway runs the sandbox on our infrastructure.

Nothing else depends on it: Tools Mode, Dynamic Mode, and `search_docs` work without Deno. A
server started in Code Mode without it warns at start-up rather than waiting for the first
`execute` to fail.

## Generating the server

The MCP server is **not** a `targets:` entry. It is configured by a top-level `mcp:` block, the
same way the docs site and the mock server are — add one to your `glotto.yml` beside your spec:

```yaml
openapi:
  source: ./openapi.yaml
targets:
  typescript: {}          # your SDKs, as usual
mcp:                      # <- the MCP server, a top-level block
  modes: [tools, code, dynamic]
  search_docs: true
```

Glotto publishes the generated server to npm for you, so `mcp` *is* a surface for **publishing**
— but the `targets:` map is the codegen surface, and putting `mcp` there fails with
[`GLOTTO_CONFIG_UNKNOWN_TARGET`](/docs/diagnostics).

With the block in place there are three ways to get a server, and they differ in what you end up
holding:

```bash
glotto generate                    # your SDKs AND the MCP server, into sdks/mcp/
glotto mcp generate --out ./server # just the server
glotto mcp serve                   # run it now, over stdio — nothing written to disk
```

- **`glotto generate`** is the one to use when the server ships beside your SDKs in the same
  repo — it is the artifact your regeneration and drift checks then cover.
- **`glotto mcp generate`** writes the server on its own, for a separate repo or image.
- **`glotto mcp serve`** starts a server straight from `glotto.yml` with no build step. This is
  the fastest way to point Claude Desktop or Cursor at your API and see the tools appear.

On the published `@glotto/cli`, `generate` and `mcp generate` run **server-side** and need
`glotto login` first; `mcp serve` runs the emitted artifact locally.

The emitted project is an ordinary npm package — `npm install && npm run build` produces
`dist/main.js`, which is what you point an MCP client at.

## Transports & auth

- **stdio** — for local clients (an `npx` entrypoint). The default.
- **Streamable HTTP** — for remote/hosted deployments, with **OAuth 2.1 + PKCE**. Selected with
  `--transport=http` (or the shorter `--http`, or `GLOTTO_MCP_TRANSPORT=http`); the listen port
  comes from `--port=N`, else `PORT`, else 3000. A flag always outranks its environment variable,
  so a container image that baked one in can still be overridden on the command line.

The HTTP transport binds **loopback (`127.0.0.1`) by default**, so a server you start on your own
machine is not reachable from the network until you say otherwise. Pass `--host=0.0.0.0` (or set
`GLOTTO_MCP_HOST`) to serve every interface. The emitted Docker image sets `0.0.0.0` for you, and
that is not an exception to the rule: inside a container that address is the container's own
network namespace, and what makes the server reachable is the port you publish with
`-p 3000:3000`. Binding loopback inside a container would break publishing rather than secure it.

On the HTTP transport the server also checks the browser `Origin` header. A request carrying an
`Origin` you have not allowlisted is refused with `403` **before** its credentials are checked,
which is what stops a page in someone's browser from resolving a hostname it controls to
`127.0.0.1` and reaching a server bound there. Requests with **no** `Origin` — every non-browser
client, so effectively all normal agent traffic — are unaffected, and the allowlist defaults to
your own `GLOTTO_MCP_RESOURCE` origin, so most deployments need no configuration. Name others
with `--allowed-origin a,b` (env `GLOTTO_MCP_ALLOWED_ORIGINS`), or pass `*` to switch the check
off where a gateway already terminates the browser connection.

How the server validates an inbound bearer depends on what your authorization server mints. For
**JWTs**, set `GLOTTO_MCP_OAUTH_JWKS_URI` and the server verifies signatures locally against your
published JWK set — no round trip per request. For **opaque** tokens, which carry nothing to verify
locally, set `GLOTTO_MCP_OAUTH_INTROSPECTION_URL` plus the client id and secret your AS issued you
and the server validates each token via RFC 7662 introspection. Set `GLOTTO_MCP_OAUTH_ISSUER` and
`GLOTTO_MCP_OAUTH_AUDIENCE` alongside either: RFC 7662 does not require an authorization server to
scope its answer to *your* server, so those checks are what prove a token was minted for you rather
than for another client of the same AS. Introspection is uncached by default, so revoking a token
takes effect on the very next request; `GLOTTO_MCP_OAUTH_INTROSPECTION_CACHE_TTL` opts into caching
positive results, and a cached result never outlives the token itself. Configure neither and the
server rejects every token — secure by default; configure both and it refuses to start rather than
silently picking one.

The two directions of auth are distinct: OAuth 2.1 + PKCE above governs how a **client
authenticates to the MCP server**. To authenticate the server's **outbound calls to your
upstream API**, Tools Mode handlers attach the API's configured credential — matching your
`glotto.yml` auth scheme (bearer / OAuth2 token, HTTP basic, or API key) — read from the
environment at runtime (the API's configured env var, else a `GLOTTO_API_*` default).

Code Mode reaches your API through **the same generated SDK**. When your `glotto.yml` builds the
TypeScript SDK alongside the MCP server, `execute`'s sandbox is handed that SDK as the module
`./sdk`, so guest code opens with the import it would use anywhere else:

```ts
import { Client } from './sdk';

const client = new Client(); // already pointed at your API, already authenticated
const pets = await client.pets.listPets();
```

`new Client()` is pre-addressed and pre-authenticated: Glotto resolves the credential
**host-side**, through the same `applyAuth` the Tools Mode handlers use — so caller-forwarded
passthrough, per-user outbound OAuth, and the operator's environment variable all reach guest code
in that same precedence order, per request. No credential belongs in the submitted code.

Binding the SDK **grants the sandbox nothing**. The credential arrives as a pre-bound request
header rather than as an environment grant, and the isolate keeps its deny-all baseline exactly as
before: no environment access, no filesystem access, and network only to your configured API host.
A guest that tries to read `Deno.env` still gets a permission denial with the SDK sitting right
there. The one refinement: guest code sees an **empty** `process.env` rather than a denial, because
the generated SDK probes it for the telemetry opt-out and for env-var credential fallbacks, and a
denial there would fault `new Client()` before it could run. Empty is the honest answer — nothing
from your server's environment is present to read, and the credential arrives as a header regardless.

When your API authenticates **per operation** (a multi-scheme `securitySchemes` registry), the
credential is scoped to the *request* rather than to the client. A client constructed before an
operation is chosen cannot know which scheme's secret to carry, so the server resolves **each**
operation's own requirement up front — through that same `applyAuth` — and the bound client attaches
only the entry matching the request it is actually making. An endpoint that asks for scheme B
receives B's credential and nothing else; a scheme's secret is never sent to an endpoint that did
not ask for it. An API key configured to travel in the query string is applied there rather than as
a header, and an operation you have blocked under `mcp.permissions` never has its credential placed
in the sandbox at all.

Two cases stay narrower, and the emitted `execute` description says which one applies rather than
promising more than the server has. Without the `typescript` target there is no SDK to bind, so
guest code calls the API with `fetch`. And when there is no credential to attach at all — an
unauthenticated API, or a registry whose every scheme is `custom` — the SDK is bound but nothing
authenticates it, so an authenticated call passes a credential in through `execute`'s `input`.

### Acting on behalf of each user (outbound OAuth)

The environment credential above is **one credential for the whole deployment** — right when you
host a server for your own team, wrong when you host one server for all of your customers. If your
API's OpenAPI spec declares an OAuth2 `authorizationCode` flow, the HTTP transport can instead run
that flow as a *client* to your API, so each caller reaches it as themselves:

```yaml
mcp:
  upstream_oauth:
    enabled: true
```

The authorization and token endpoints come from your **spec's** `flows.authorizationCode` — there
is nothing to re-declare here, and nothing to keep in sync by hand. Register your server with your
provider and give it `GLOTTO_MCP_UPSTREAM_CLIENT_ID` / `GLOTTO_MCP_UPSTREAM_CLIENT_SECRET`; secrets
never belong in `glotto.yml`. On a multi-scheme API, name the scheme the flow satisfies
(`upstream_oauth.scheme`) — the server will not guess which credential a user's token stands in for.

Every authorization and token request also names your API as its RFC 8707 `resource`, so a provider
that supports resource indicators can issue each user a token restricted to your API rather than one
valid everywhere that provider is trusted. It is derived from the environment URL already in your
`glotto.yml` — nothing to configure — and providers that do not implement the parameter are required
to ignore it, so sending it is safe either way.

Consent is **three steps**, and the third one is the point. The agent calls
`/oauth/upstream/authorize` with its own access token and gets a redirect to your provider's consent
screen. Your provider redirects back to `/oauth/upstream/callback`, which exchanges the code and —
storing nothing yet — shows the user a single-use **claim code**. The agent then posts that code to
`/oauth/upstream/confirm`, again with its own access token, and only that call writes a token to the
vault:

```sh
curl -X POST https://your-server.example.com/oauth/upstream/confirm \
  -H "Authorization: Bearer $AGENT_ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"claim_code":"<the code from the consent screen>"}'
```

The extra step exists because starting an authorization is not the same as granting one. Without it,
any authenticated caller could hold a live `state`, get one of your users to complete the real
consent screen, and receive that user's credential. The claim code only ever reaches the browser
that saw the consent screen, so holding it is the proof. If the confirming account is not the one
that started the flow, the grant is discarded with a `403` rather than re-homed — finishing an
authorization on behalf of someone who never started it is the outcome this prevents.

Each user's tokens are keyed on the **verified** subject of their inbound access token — never on a
value the caller supplies — and access tokens refresh automatically, including against providers
that rotate the refresh token on every exchange.

Tokens live behind a `TokenVault` interface. The shipped default holds them **in memory**, so every
user re-consents after a restart; implement the interface to back it with Redis or a database.

This is opt-in and HTTP-only: with the flag off, or on stdio, the environment credential above is
unchanged.

If your API has **no** OAuth at all, the per-caller passthrough below is the other way to lift the
same single-tenant assumption. They are alternatives, not layers: arm both and the server says so
at start-up, passthrough wins, and the per-user tokens it vaults are never sent upstream.

### Per-caller credentials (remote deployments)

Sourcing the outbound credential from the environment welds one deployment to one credential —
right for a team hosting a server for itself, wrong for an API company hosting **one** server for
all of its customers. Setting `GLOTTO_MCP_CREDENTIAL_PASSTHROUGH=1` on an HTTP deployment forwards
**the caller's** credential upstream instead, so a single deployment serves many users with no
OAuth machinery:

```json
{
  "mcpServers": {
    "my-org-mcp": {
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "the-caller's-credential" }
    }
  }
}
```

It is off by default because it changes the trust model, and three properties are worth knowing
before you arm it:

- **It replaces the inbound OAuth bearer gate.** The inbound credential is forwarded upstream
  rather than verified as an MCP access token — your API becomes the authority. The server is not
  thereby unauthenticated: armed, it holds no credential of its own to spend.
- **It fails closed.** A request that forwards no credential is refused with `401`. The
  environment credential is never used to serve a caller who supplied none, so an anonymous caller
  can never spend yours.
- **Multi-scheme APIs name the scheme.** With one scheme the credential rides `Authorization`,
  used verbatim. With several, each scheme has its own `x-glotto-upstream-<scheme>` header —
  `Authorization` cannot say which scheme it satisfies, so it is not consulted. The emitted
  README lists the exact headers your API accepts.

HTTP only, on the standalone transport and the embedded mounts alike; stdio is a local
single-user process where the environment credential is already correct.

#### Capping which schemes may be caller-sourced

The env var is an **operator's** switch, and on a multi-scheme API it arms every scheme at once —
including one whose credential is the deployment's rather than the caller's, like a partner key or
a signing secret. `mcp.credential_passthrough` is the **author's** half: a ceiling on which schemes
may *ever* be caller-sourced, which the operator's switch can only fill, never widen.

```yaml
mcp:
  credential_passthrough:
    schemes: [userAuth] # only this scheme may be sourced from the caller
```

Config declares, env enables. Three properties follow, and the third is the one to read closely:

- **It only ever narrows.** The env var stays the sole arming switch; the ceiling never turns
  passthrough on, and with no `credential_passthrough` block nothing changes.
- **It cannot be widened at runtime.** The cap is baked in at generation time — the emitted server
  contains no code path to a forwarded credential for a scheme you did not name, so there is
  nothing an environment variable or flag could switch back on.
- **A capped scheme keeps spending your credential.** This is the deliberate exception to the
  fail-closed rule above: while passthrough is armed, a scheme outside the ceiling still sources
  from its environment variable, and a forwarded header for it is ignored. That is the point — you
  declared that credential yours to spend — but it does mean any caller the 401 gate admits reaches
  your API as *your deployment* for those schemes. Cap a scheme because it should be yours to
  spend, not as a way to keep callers away from an operation.

The block is an **allow-list**, so it resolves fail-closed: writing `credential_passthrough` at all
declares a ceiling, and a name that matches no scheme in your spec simply admits nothing rather
than falling back to admitting everything. Run the server with `--debug` to see what it resolved:

```text
  passthrough         userAuth  (mcp.credential_passthrough ceiling — 1 of 3)
```

If your API **does** declare an OAuth `authorizationCode` flow, prefer the outbound flow above:
it binds each user's credential to an identity your server cryptographically verified, where
passthrough trusts whatever the caller sends. Reach for passthrough when there is no OAuth to run.

Tool filtering at start (`--resource`, `--operation read|write`, `--tag`) lets a client
subset a large API; a filtered-out operation is invisible in Tools Mode and to all three
Dynamic Mode meta-tools alike.

`--operation read|write` splits on **HTTP method safety** (RFC 9110), the same partition the
[tool annotations](#tool-annotations) below publish — so `GET`, `HEAD`, `OPTIONS` and `TRACE`
are all reads, and only the verbs that can actually change something are writes. Stand up a
read-only deployment with `--operation read` and a `HEAD` operation comes with it.

## Debugging what the server actually resolved

Four `glotto.yml` keys can quietly beat a runtime flag — `mcp.modes` clamps `--mode` to an
enabled mode, `mcp.filters`' `locked` set overrides `--resource`/`--operation`/`--tag`,
`mcp.permissions` removes operations before the filters ever see them, and
`mcp.credential_passthrough` caps what `GLOTTO_MCP_CREDENTIAL_PASSTHROUGH` can reach — and from
outside the process none of it is visible. `--debug` (or `GLOTTO_MCP_DEBUG=1`) prints what the server
resolved, naming the key that won, then starts normally:

```text
glotto mcp debug — petstore-mcp
  transport           stdio  (default)
  mode                tools  (requested "code" is not enabled by mcp.modes)
  modes enabled       tools, dynamic
  filters             operation=read  (mcp.filters resolved over the runtime flags "operation=write")
  tools               2 of 3 registered  (mcp.permissions denies 1 of 3)
  search_docs         registered
  sandbox binary      deno  (default)
  sandbox allow-net   api.petstore.example
```

The report goes to **stderr**, never stdout — on the stdio transport stdout carries the MCP
protocol itself, so it stays clean whether or not you pass the flag.

## Tool annotations

Every operation tool is annotated with the standard MCP hints, derived from the operation's
HTTP method. You configure nothing — they come from the same spec the tools do:

| HTTP method | `readOnlyHint` | `idempotentHint` | `destructiveHint` |
|---|---|---|---|
| `GET`, `HEAD`, `OPTIONS`, `TRACE` | `true` | `true` | `false` |
| `POST` | `false` | `false` | `false` |
| `PUT` | `false` | `true` | `true` |
| `PATCH` | `false` | `false` | `true` |
| `DELETE` | `false` | `true` | `true` |

`POST` is the one write verb marked non-destructive: it is the verb that *creates*, where
`PUT` replaces, `PATCH` modifies, and `DELETE` removes state that already exists.

MCP clients increasingly use these hints to decide what an agent may run unattended and what
needs a human to confirm — auto-running a list call while pausing on a delete. Tools Mode
publishes them on `tools/list`; in Dynamic Mode, `describe_tools` reports the same values.

Two deliberate choices worth knowing:

- **`openWorldHint` is never set.** Whether an operation reaches the wider internet is a
  property of your API's *implementation*, and an OpenAPI document does not describe it. Rather
  than guess, the server leaves the hint unset and your client's own default applies.
- **An unrecognized HTTP method is annotated at maximum restriction** — not read-only, not
  idempotent, destructive — rather than left bare, so an unusual verb is never mistaken for a
  safe one.

> Annotations are advice to a client, not enforcement. A client is free to ignore them, and
> nothing about them prevents a call. To constrain what the server can reach at all, use
> **method permissions** below; to enforce access per request, use a scoped API token or the
> Glotto MCP Cloud gateway.

## Method permissions

Filters are an *operator* convenience an operator can also widen back. When you want the
server to be **born** unable to reach certain operations — a self-hosted deployment that
should only read, say — declare a permission set in `glotto.yml`:

```yaml
mcp:
  permissions:
    allow_http_gets: true              # every operation mapped to HTTP GET
    allowed_methods:                   # regexes over the qualified method name
      - pets\.photos\..*
    blocked_methods:                   # applied last — beats both allow keys
      - pets\.delete
```

The name a pattern matches is the operation's **fully-qualified method name** —
`<resource path>.<method>`, dotted through subresources: `pets.list`, `pets.photos.add`.
Patterns are fully anchored, so `pets\.get` matches `pets.get` and not `pets.getAll`. An
invalid regex fails `glotto generate` rather than the running server.

Resolution mirrors the shape you may know from Stainless:

1. The allow set is **constrained** only if `allowed_methods` has a pattern or
   `allow_http_gets` is `true`; a method is in it if it matches a pattern **or** is a GET
   under `allow_http_gets`.
2. `blocked_methods` is subtracted **after** — a method both allowed and blocked is denied.
3. With **neither allow key set**, everything not blocked is permitted.

Enforcement differs by mode, and the difference matters:

- **Tools Mode and Dynamic Mode** — a denied operation is simply never registered.
  `list_tools` omits it, `describe_tools` calls it unknown, `invoke_tool` refuses it. Nothing
  at runtime widens the set: no `--resource` flag, env var, or `mcp.filters` value can add
  back a method your `glotto.yml` denies.
- **Code Mode** — `execute` statically scans the submitted TypeScript **before running it**
  and refuses, without reaching the sandbox, when the code references a denied operation by
  qualified name or by request path.

> **Method permissions are a convenience layer, not a security boundary.**
> In Tools and Dynamic Mode the gate is structural — an unregistered tool cannot be called.
> In Code Mode it is *static analysis of guest code*, and static analysis can be circumvented:
> dynamically constructed URLs, indirection, and deliberate obfuscation all defeat it. Use
> permissions to keep a well-behaved agent inside its lane. To actually protect sensitive API
> operations, use API-layer authentication with a **scoped API token or restricted API key**,
> or put the server behind the **Glotto MCP Cloud gateway**, where access control is enforced
> per request.

## Experimental: async tasks

The `glotto.yml#/mcp/experimental/async_tasks` flag opts the operations your spec marks
long-running (a `202`-accepted response or an explicit `x-polling` extension) into **async
task-capable tools**. The flag is **experimental, default off, and revision-tracked** —
and that revision tracking just fired exactly as designed: the final **2026-07-28 MCP spec**
redesigned async tasks as the official `io.modelcontextprotocol/tasks` extension and retired
the experimental 2025-11-25 (SEP-1686) shape the flag previously emitted.

The flagged emission is being rebuilt for the extension shape. Until that lands, enabling the
flag for an API with long-running operations **fails generation with an actionable error**
rather than emitting a server on the retired shape; with the flag absent or `false` (the
default — and the only state to ship to consumers) the emitted server carries no task surface
and is unaffected.

## Install & distribution

The emitted server is a **publishable npm package** (no `private` flag, a runnable `bin`, a
dist-only `files` list) and ships with every mainstream install affordance:

- **README install blocks** — a `claude mcp add` command, a generic `.mcp.json` snippet, a
  Cursor install deep link, and an `npx` quickstart naming the exact env vars the server reads.
- **npm publish** — the release flow publishes it to npm exactly as it does your SDKs
  (`--all` includes it automatically whenever your `glotto.yml` has an `mcp` block).
- **Docker** — an emitted `Dockerfile` runs the server over stdio (`docker run -i`) or
  Streamable HTTP (`GLOTTO_MCP_TRANSPORT=http`).
- **Claude Desktop one-click install** — an emitted [MCPB](https://github.com/anthropics/mcpb)
  `manifest.json`; `npx -y @anthropic-ai/mcpb pack` produces the `.mcpb` bundle, and the
  installer prompts for the API credential instead of baking it in.
- **Official MCP registry** — set `mcp.registry_name` (your reverse-DNS registry namespace) and
  Glotto emits a `server.json` for
  [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io).

Your [generated docs site](/docs/generated-docs-site) closes the loop with a `/connect-mcp/`
page — the same install blocks, linked from the site nav, so API consumers can connect an AI
client without leaving your docs.
