# Connect an agent (/en/docs/mcp)

The same engine as the REST API, a different front door: instead of a person typing `curl`, a
third-party AI agent calls typed tools.

```
https://mcp.facturia.cl
```

One MCP endpoint (`/mcp`) speaking **Streamable HTTP**, and one set of OAuth endpoints on the same
origin. There are no per-empresa or per-account subdomains: tenancy is a claim in the token, not a
hostname.

## The architectural fact [#the-architectural-fact]

> **The MCP server is a REST client of /v1**
>
> It holds no business logic and none of the engine's credentials: no SII (Chile's tax authority)
> certificate, no signing key, no access to the engine's schema. Every domain question — what a
> document's totals are, whether folios (the pre-authorized numbers a document must carry) are
> available, what the F29 (the monthly VAT return) says — is a request to `/v1`, never a query.
>
> Which means everything in these docs about statuses, errors and guarantees holds exactly the same
> from an agent.

## Why an agent is not a developer [#why-an-agent-is-not-a-developer]

A developer reads the documentation before writing the first line; an agent decides what to call in
the same turn it is asked. This server publishes its pages as **MCP resources** — returned whole,
not as a link, so a client with no fetch capability reads them too — and even so every tool has to
describe itself: the documentation explains the model, and the tool description is all the model has
in hand at the moment it decides. Three design commitments for this layer follow from that:

**The full suite, not a thin wrapper.** Emission, reception and the tax layer are all here.

**Direct execution, with an escape hatch for whoever wants one.** The MCP client's own tool-approval
step is the default human in the loop. Empresas that want more can turn on a two-step
draft-and-confirm.

**Being AI-first means resolving ambiguity, not refusing it.** `draft_document` takes a receptor
(the recipient) by name, because that is how a person talks to an agent. Line items are structured
(`[{description, quantity, unit_price}]`): the model does the prose, because it understands prose
better and more cheaply than any parser we could write, and `draft_document` returns the totals so
the person sees them **before** anything is signed. There is no prose parser in the API and there
will not be one: a parser hidden behind an endpoint turns a misread quantity into a tax document.

## Add it to your client [#add-it-to-your-client]

One address, `https://mcp.facturia.cl/mcp`, and nothing to install: the transport is
**Streamable HTTP**, so there is no package to download and no local process to run. There is no
secret to paste into a config file either — the client discovers OAuth on its own (RFC 9728),
identifies itself with its metadata document or registers itself (RFC 7591), and opens the consent
screen in your browser, where you decide what the agent may do and over which empresas.

#### Claude (mcp.json)

In Claude Desktop: **Settings → Connectors → Add custom connector**, with the URL
`https://mcp.facturia.cl/mcp`. If you would rather edit the file by hand
(`claude_desktop_config.json`), it is one line:

```json
{
  "mcpServers": {
    "facturia": { "url": "https://mcp.facturia.cl/mcp" }
  }
}
```

#### Claude Code

```bash
claude mcp add --transport http facturia https://mcp.facturia.cl/mcp
```

The first tool call opens the browser to authorize. After that, `claude mcp list` shows the
connection and `claude mcp remove facturia` drops it from this client (to actually cut off access,
revoke it from the dashboard — see [Revocation](#revocación)).

#### claude.ai

On the web: **Settings → Connectors → Add custom connector**, with the same URL
`https://mcp.facturia.cl/mcp`. There is no file to edit; authorization opens right there.

#### Cursor

In `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project), the same object:

```json
{
  "mcpServers": {
    "facturia": { "url": "https://mcp.facturia.cl/mcp" }
  }
}
```

Cursor lists the server under **Settings → MCP**; authorization opens in the browser the first time
the agent calls a tool.

#### VS Code

With GitHub Copilot in agent mode, in the project's `.vscode/mcp.json` (or in user settings), the
same remote server:

```json
{
  "servers": {
    "facturia": { "type": "http", "url": "https://mcp.facturia.cl/mcp" }
  }
}
```

VS Code opens the browser to authorize the first time the agent uses a tool.

#### Windsurf

In `~/.codeium/windsurf/mcp_config.json`, the same object as Claude and Cursor:

```json
{
  "mcpServers": {
    "facturia": { "serverUrl": "https://mcp.facturia.cl/mcp" }
  }
}
```

#### ChatGPT

In ChatGPT, MCP servers are added as a **connector**: settings → connectors → add, pointing at
`https://mcp.facturia.cl/mcp`. Availability depends on the plan and the mode (custom connectors are
on the paid plans and in developer mode).

Any client that speaks MCP over Streamable HTTP with OAuth works just as well: there is nothing
client-specific in this server.

> **Never created an account?**
>
> The consent screen lets you sign in right there and mints the grant's key for you — you do not have
> to bring one ready. And if you have no account yet, the link on that screen takes you to create one
> and **brings you back to the same authorization** when you are done: no reconnecting the agent.
>
> No empresas yet either? Also fine: tick "Todas las empresas de esta cuenta, incluidas las que cree
> más adelante" together with `empresas:write`, and the agent creates the first one with
> `create_empresa`.

Typing `https://mcp.facturia.cl` into a browser gives you the same information as a page:
what it is, the snippet, and the link to these docs.

**How the client identifies itself.** The MCP spec sets a priority order for how a client says who
it is: a pre-registered client first, then a client ID metadata document (CIMD), and dynamic
registration last, as the fallback.

* **Client ID metadata document (CIMD).** The client can use an https URL as its `client_id` — for
  example `https://claude.ai/.well-known/oauth-client.json` — instead of registering before it
  connects. The server fetches it at connect time (GET, 3 s timeout, 32 KB max, no redirects, never
  to a private or loopback address) and validates the JSON: the `client_id` the document declares
  has to be exactly that URL, `client_name` and `redirect_uris` are required, and only public clients
  are accepted (`token_endpoint_auth_method` absent or `"none"`, PKCE S256 as always). The result is
  cached honoring `Cache-Control: max-age`, clamped between 1 and 24 hours — an error is never
  cached. The `redirect_uri` of every authorization has to match one the document declares exactly,
  the same rule as a registered client — with one exception, RFC 8252 §7.3's: when the document declares `http://localhost/…` or
  `http://127.0.0.1/…`, the port is not compared, because a native client such as Claude Code opens a
  different one every session.
* **Dynamic client registration (RFC 7591).** The fallback for a client that does not yet publish a
  CIMD — Cursor, VS Code, Windsurf, ChatGPT and similar still register this way. Any MCP client
  registers itself on first connection — no prior approval, no application allowlist. Registration
  returns a public `client_id`; there is no `client_secret`, because PKCE is the confidentiality
  mechanism for a public client that cannot keep a secret.

```
POST https://mcp.facturia.cl/register        (RFC 7591)
GET  https://mcp.facturia.cl/.well-known/oauth-authorization-server
```

That same `GET` declares whether the server accepts CIMD:

```json
{ "client_id_metadata_document_supported": true }
```

**What the person sees on the consent screen.** With a CIMD client the screen shows its name and the
origin of its metadata document ("Claude · claude.ai") — that origin is verifiable, because that is
where the document lives. A dynamically registered client still gets the warning "This application
registered itself and picked its own name — we do not verify it." In both cases the screen keeps
showing the host the authorization code will be sent to ("Where this is going"), with an extra
warning when that host is `localhost` or `127.0.0.1`.

**Authorization and consent.** Standard authorization-code + PKCE flow. The consent screen is not a
rubber stamp: that is where you decide which Facturia credential the grant wraps.

```
GET https://mcp.facturia.cl/authorize
  ?client_id=mcpc_01K2SC9X4M
  &redirect_uri=https://claude.ai/api/mcp/auth_callback
  &response_type=code
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &scope=documents:read documents:write tax:read
  &state=af0ifjsldkj
```

**Token.** `POST /token` exchanges the code (with the PKCE verifier) for a short-lived access token
and a longer-lived refresh token, revocable on its own.

```json
{
  "access_token": "mcpat_9lQpV3fA2mKcRt7wZxYb1Nde",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "mcprt_qT8vN2wLmR5xKpJ7hYdCbA3s",
  "scope": "documents:read documents:write tax:read"
}
```

## What the token carries [#what-the-token-carries]

The same authorization model as an API key, because it has to behave the same once issued:

* an **account**,
* a **mode** — the token can be sandbox-only, live or mixed, depending on what was granted;
* a **set of scopes**, with the same vocabulary as the REST scopes: `documents:read`,
  `documents:write`, `received:read`, `received:write`, `tax:read`, `tax:write`, `empresas:read`,
  `usage:read` and, for going live, `onboarding:write`, `mandato:write` and `empresas:write`;
* an **empresa allowlist** — explicit RUTs (Chile's taxpayer ids), or `"*"`.

> **The two attestation scopes do not exist on this surface**
>
> `/v1` also defines `mandato:attest` and `onboarding:attest`: accepting the mandato — the power of
> attorney that lets Facturia act for a company before the SII — with a request body, and authorizing
> the go-live with one. Those are the moment a **person** says "yes, I accept" and "yes, this month",
> and they are recorded as the evidence of that decision.
>
> An agent cannot ask for them here, and that is not an oversight: the MCP tools mint the link and
> stop; the person decides on the page. If the agent could attest, the record of your approval would
> be the agent's word for it.

## Naming the empresa in a tool call [#naming-the-empresa-in-a-tool-call]

A single token's list can span several empresas (an accountant's agent handling several clients,
say), so every empresa-scoped tool takes an explicit `empresa` argument — the direct analogue of the
`Facturia-Empresa` header.

| Situation                                                             | Behavior                                         |
| --------------------------------------------------------------------- | ------------------------------------------------ |
| Omitted where it is required                                          | The tool returns `empresa_required`              |
| Present but outside the list                                          | `empresa_not_allowed` — never a bare "not found" |
| A token with exactly one empresa                                      | You may omit it; the server fills it in          |
| Account-level tools (`list_empresas`, `get_usage` without an empresa) | Reject the argument                              |

## The sandbox limit [#the-sandbox-limit]

> **Trap — A sandbox grant is a capability, not a policy**
>
> A sandbox-scoped token **cannot** reach a live empresa's data or issue anything with legal effect.
> It is not a preference that can be flipped at runtime: it is a structural limit.
>
> That is why it is the right default for anything exploratory or public-facing. Dynamic client
> registration means the calling software is **unverified by construction**, and the token's scope is
> the primary defense against a compromised or over-trusting agent.

The consent screen defends the same thing from the other side: nothing comes pre-checked. You pick
explicitly which empresas this agent may touch. Granting `"*"` is possible but takes an extra
confirmation click — the asymmetry is deliberate, because a grant to an over-trusting third-party
agent is a materially different risk from a first-party API key kept in your own backend.

## Revocation [#revocación]

In the dashboard, **Connected agents** lists every MCP grant on the account: which client connected,
with which scopes, over which empresas, in which mode, when, and when it last used it. Revoking one
invalidates its access and refresh tokens immediately.

There is a second path that works just as well: every grant wraps one of your API keys, so revoking
that key under **Keys** kills the grant in the same instant.

## Rate limits [#rate-limits]

MCP calls hit the same backend services as the REST ones and share the same published buckets. There
is no separate MCP quota, because there is no separate emission pipeline underneath.

| Bucket                  | Limit                  | Which tools                                                                                                                                                                                                       |
| ----------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General                 | 300 req/min            | `draft_document`, all the `list_*` / `get_*`, `query_rcv`, `get_periodo_summary`, and the writes that neither emit nor file (`update_*`, `set_*`, `characterize_purchases`, `send_document`, `request_folios`, …) |
| Emission                | 10 req/s, burst 30     | `emit_document`, `emit_tipo46`, `emit_credit_note`                                                                                                                                                                |
| F29 filing and RCV sync | 10 req/min per empresa | `file_f29`, `sync_rcv` (one call per period; a ten-month range spends the whole bucket)                                                                                                                           |
| Artifacts               | 60 req/min             | `get_document_file` (minting the signed URL, not the download afterwards, which is public and unmetered)                                                                                                          |

A throttled call returns the `rate_limit_error` type with a `retry_after_seconds` field an agent can
act on directly.

- [Tool catalog](/en/docs/mcp/herramientas): Every tool on the server, with its annotations and which ones go through confirmation.

- [Confirmations](/en/docs/mcp/confirmaciones): The two-step for emitting, filing and claiming.
