# Foliora MCP for customer-owned agents

Version: 0.5.0

Canonical MCP endpoint: https://www.foliora.ai/mcp

Canonical agent contract: https://www.foliora.ai/agents/foliora-mcp.md

Product source: https://www.foliora.ai/llms.txt

Human overview: https://www.foliora.ai/mcp

Foliora is Managed AI Search. It researches buyer questions, builds exact page
changes, keeps the customer in control of approval, publishes through a native
publisher or customer-owned agent, and verifies the rendered production page.

This document is the machine-readable operating contract for agents connecting
to Foliora. It describes what each tool may do, which credentials it needs, and
the boundary an agent must preserve.

## The contract in one paragraph

Five tools, one row shape. Three lists read one site — `list_pending_jobs`,
`list_approved_jobs`, `list_rejected_jobs` — and every row carries its
content: the exact operations to put on the page (or, for a site-health
finding, an instruction), a `hash`, a `status` in the customer's own words,
and `nextAction`, the only thing to do with it. Two calls record the
customer's decisions for several rows at once: `approve_apply` and
`reject_apply`. siteId is required on every call; the customer has it from
their dashboard's **Connect your agent** page, and an agent never guesses
which site it is working.

## How to work with a customer — the playbook

Read this before calling any authenticated tool. The customer is usually not
an SEO expert; you are acting with their credentials on their live website.
Every rule below exists because breaking it damages a real site or lies to a
real person.

**1. Read the site, tell the customer where things stand.** Call
`list_pending_jobs` with the customer's `siteId`. Tell them each row's
`status` in the word it carries — *working*, *yours*, *published*,
*verified*, *snag* — and its `label`. `site.publisher` says who puts an
approved change on the page: `foliora` (a connected Shopify or WordPress
site; Foliora publishes and verifies) or `you` (the agent applies, with the
site credentials it already holds).

**2. Do what `nextAction` says, and nothing else.**

| `nextAction` | Do |
| --- | --- |
| `approve_apply` | The draft is ready. Show the customer every operation's exact `before` and `after`. If they say approve: on a `you` site, put every `after` value on the page first, in ordinal order, then call `approve_apply` with the row's `id` and `hash`; on a `foliora` site, call `approve_apply` at once and do not touch the page. |
| `apply` | (In `list_approved_jobs`.) The customer approved this on the dashboard and it is not on the page yet. Put every `after` value on the page, then call `approve_apply` with its `id` and `hash`. |
| `fix` | A site-health finding no draft carries. Fix it on the site as `instruction` says, then call `approve_apply` with its `id` and no hash. |
| `wait` | Foliora is drafting, publishing, verifying or re-checking. Do nothing; do not poll. |
| `none` | A snag only the dashboard can resolve, or history. Tell the customer what `reason` says. |

**3. Approving is the customer's decision, never yours.** Call
`approve_apply` only when they say approve, for the rows they named, with the
row's `hash`. The hash is what makes "you approved exactly this" true: a
changed draft has a new hash and the old one is refused, for that item only.
Never approve a row whose `nextAction` is not `approve_apply`.

**4. Apply exactly.** No rewording, no extra edits, no "improvements". Each
operation names its page, its place (`selector`), what is there now
(`before`) and what goes there (`after`). If the page does not match an
operation's `before`, stop and tell the customer; do not force it.

**5. Foliora verifies; you do not.** `approve_apply` on a `you` site records
the approval and the receipt together, and Foliora reads the live page on
its own. A row that comes back *snag* was read and not found: fix the page
and call `approve_apply` again for that row — it re-files the same receipt
and Foliora reads again. There is no tool to verify, recrawl or re-ask, by
design.

**6. Declining.** When the customer says no, `reject_apply` with the rows
they named and, if they gave one, their reason. A draft closes; Foliora may
draft the page again in a later cycle. A finding is ignored.

**7.** Never send site, CMS, hosting or GitHub credentials to a Foliora tool.
Never claim a change is live, verified or cited unless its `status` says so.
Never report a finding you did not fix, and never rewrite page copy from a
finding — copy changes go through a drafted change.

## Non-negotiable execution boundary

Foliora's workflow is Research → Build → Approve → Publish → Verify.

- A human approves the exact versioned change. That can happen in the Foliora
  dashboard, or through `approve_apply` when the customer has read the
  drafted operations with their agent and told it to approve that exact hash.
  MCP never approves on its own initiative: an agent calls `approve_apply`
  only on the customer's instruction, with the hash the customer reviewed,
  through the customer's own credential.
- Every list row carries the drafted operations with exact before and after
  values, for that review.
- A customer-owned agent applies those exact operations using site credentials
  it already holds. Never send CMS, GitHub, hosting, or site credentials to a
  Foliora MCP tool.
- `approve_apply` files the agent's receipt bound to the same hash and the
  exact stored values — every approved ordinal exactly once. Foliora rejects a
  stale hash, and a receipt is never filed against operations other than the
  ones approved.
- Foliora then re-reads and verifies the rendered production page.
- Rollback remains a human recovery action in the Foliora dashboard. MCP does
  not expose rollback, and a verification mismatch never rolls back
  automatically.

Do not regenerate, summarize, improve, or reinterpret an approved operation.
If the current page no longer matches the approved before-state, stop and send
the customer back to Foliora instead of forcing the write.

## Transport

Use Streamable HTTP at:

```text
https://www.foliora.ai/mcp
```

The server supports the initialize-based MCP versions from `2025-03-26`
through `2025-11-25` and the stateless `2026-07-28` protocol. Use the protocol
headers and request metadata required by the negotiated MCP version. Request
bodies are limited to 64 KiB. JSON-RPC batches are accepted only for the legacy
`2025-03-26` contract.

`/mcp` asks for a credential when you connect: an unauthenticated
`initialize` returns `401` with the OAuth protected-resource challenge, so
your client offers a sign-in before it reports the server as connected. For
the three public discovery tools without any account, connect to
`https://www.foliora.ai/mcp/public` instead.

## Authentication

The three public discovery tools require no credentials on `/mcp/public`.
Account and execution tools require one of these Bearer credentials:

### OAuth 2.1

OAuth is the preferred interactive path for ChatGPT, MCP clients, and other
agents acting for a signed-in Foliora customer.

The MCP server advertises protected-resource metadata at:

```text
https://www.foliora.ai/.well-known/oauth-protected-resource/mcp
```

Follow the `authorization_servers` entry in that document. Requested OAuth
scopes are `openid email`. The customer authenticates with Foliora and reviews
the client and requested access before authorizing it.

When an account tool is called without credentials, the server returns HTTP
`401` with a `WWW-Authenticate` challenge containing the protected-resource
metadata URL.

### Foliora API key

A customer can create a named key in:

```text
https://www.foliora.ai/dashboard/settings
```

Keys start with `foliora_`, are displayed once, and are stored by Foliora only
as SHA-256 digests. Send one only in the HTTP header:

```http
Authorization: Bearer foliora_REDACTED
```

Never put a key in a tool argument, URL, log, chat message, or publication
receipt. Customers can revoke keys from Account settings.

API-key scopes:

- `read` — the three lists.
- `execute` — includes `read`; kept for keys created before the five-tool
  contract.
- `approve` — includes `read`, plus `approve_apply` and `reject_apply` on the
  customer's instruction.

### Connecting from Claude Code, Cursor or Codex

With OAuth (the customer signs in to Foliora in the browser on first use):

```bash
claude mcp add --transport http foliora https://www.foliora.ai/mcp
```

Cursor and other clients that connect from a config file: add the server at
`https://www.foliora.ai/mcp`; the first connection returns the OAuth
challenge and the client opens the sign-in. If your client cannot run OAuth,
use an API key header as below.

With an API key created in Account settings:

```bash
claude mcp add --transport http foliora https://www.foliora.ai/mcp \
  --header "Authorization: Bearer foliora_REDACTED"
```

The loop is then: `list_pending_jobs` → the customer reads the operations →
they approve → (on a `you` site) put the exact `after` values on the page →
`approve_apply`. Foliora verifies the live page itself and the row reads
*verified*; the next scheduled research run shows what changed.

## Public tools

### `get_product`

Authentication: none.

Arguments: `{}`

Returns Foliora product facts, the workflow, per-site plans, canonical URLs,
and product boundaries. It does not crawl a site.

### `create_preview_link`

Authentication: none.

Arguments:

```json
{ "websiteUrl": "https://example.com" }
```

Returns a normalized public website URL and a Foliora `/preview?website=` URL.
This tool does not start a crawl. Send the human to the returned URL so they can
sign in and start the preview.

### `get_snapshot`

Authentication: none. The snapshot token is an unguessable public capability.

Arguments:

```json
{ "token": "32 hexadecimal characters" }
```

Returns the public snapshot the human already started: status, bounded pages,
findings, and sampled visibility. A failed sample is `unsampled`, never
`absent`. Foliora samples ChatGPT and Gemini; do not describe other assistants
as part of the default panel.

## Customer tools

All five require a credential: OAuth, or an API key with the scope named.
`siteId` is required on every call.

### The row

Every list returns `{ site, jobs, nextCursor }`. `site` is
`{ siteId, host, publisher }`, `publisher` being `foliora` or `you`. Each
row in `jobs`:

```json
{
  "id": "uuid — a job or a finding",
  "kind": "job | finding",
  "page": "https://example.com/pricing",
  "title": "Answer the pricing question",
  "why": "why Foliora proposes it, in one or two sentences",
  "status": "working | yours | published | verified | snag",
  "label": "the customer's dashboard wording of that status",
  "reason": "null, or why there is nothing to act on",
  "nextAction": "approve_apply | apply | fix | wait | none",
  "hash": "64 lowercase hex characters, or null when nothing can be approved",
  "operations": [
    {
      "ordinal": 1,
      "kind": "set_title | set_meta_description | set_heading | replace_section | prepend_section | append_faq | set_json_ld | set_robots_directive | set_image_alt | set_redirect_target | create_page",
      "targetUrl": "https://example.com/pricing",
      "selector": { "heading": "Pricing" },
      "before": "the stored current value",
      "after": "the exact approved value"
    }
  ],
  "instruction": "for a finding: what to change on the site; null on a job",
  "updatedAt": "ISO timestamp or null"
}
```

`operations` are empty on a row still being drafted (*working*) and on a
finding. Lists take `limit` (1–50, default 20) and `cursor` (the
`nextCursor` of the previous page).

### `list_pending_jobs`

Scope: `read`. Arguments: `{ "siteId": "uuid", "limit": 20, "cursor": "…" }`

What Foliora has drafted for the site that the customer has not decided on,
highest impact first, then the site-health findings an agent can fix. A row
whose `nextAction` is `approve_apply` is ready to show the customer.

### `list_approved_jobs`

Scope: `read`. Same arguments.

What the customer approved, newest first, with where it stands: *yours*
(approved on the dashboard, not on the page yet — `nextAction` `apply`),
*published* (on the page, awaiting Foliora's read), *verified* (read live),
*snag* (read and not found — fix the page, `approve_apply` again). Reported
finding fixes appear here too.

### `list_rejected_jobs`

Scope: `read`. Same arguments.

What the customer declined, newest first, with the content they declined.
History only: nothing here can be approved.

### `approve_apply`

Scope: `approve`. One call per batch, on the customer's instruction only.

```json
{
  "siteId": "uuid",
  "items": [
    { "id": "job uuid", "hash": "the row's hash" },
    { "id": "finding uuid", "note": "what you changed, optional" }
  ]
}
```

For a job item the customer approved exactly the operations shown, by the
row's `hash`. On a `you` site, put every operation's exact `after` value on
the page first, then call this: it records the approval and the receipt
together and Foliora verifies the live page. On a `foliora` site, call it as
soon as the customer approves; never edit the page. Calling it again for a
row already approved re-files the receipt (after a *snag*). For a finding
item, fix it on the site first, then pass its id with no hash.

Returns `{ "results": [ … ] }`, one per item, in order:

```json
{ "id": "uuid", "kind": "job", "ok": true, "outcome": "approved_and_recorded", "status": "published", "reason": null }
```

`outcome` is one of `approved_foliora_publishes`, `approved_and_recorded`,
`recorded` (an already-approved row's receipt), `fix_recorded`, or `refused`
with a `reason` (`stale_hash`, `hash_required`, `no_draft`, `rejected`,
`not_on_this_site`, or the row's own reason). A refused item never stops the
rest of the batch.

### `reject_apply`

Scope: `approve`. One call per batch, on the customer's instruction only.

```json
{ "siteId": "uuid", "items": [ { "id": "uuid", "reason": "in the customer's words, optional" } ] }
```

A job row is rejected: its draft closes and the page stays as it is; Foliora
may draft the page again later. A finding row is ignored. Returns the same
`results` shape with `outcome` `rejected`, `ignored` or `refused`.

## Errors

MCP tool errors use stable public codes. Do not depend on database text.

- `invalid_input` — malformed arguments, stale approval, receipt mismatch, or
  execution conflict. A stable `reason` may provide the narrower condition.
- `not_found` — an unknown resource or an ID outside the authenticated account.
- `sign_in_required` — the account tool needs OAuth or an API key.
- `rate_limited` — retry after the current rate-limit window.
- `service_unavailable` — the authenticated service or durable limiter is not
  available. Do not retry in a tight loop.

The transport may return HTTP `401`, `403`, `429`, or `503` before a tool result
when authentication, origin validation, rate limiting, or a dependency prevents
execution.

## Rate limits

Current one-minute limits:

- 60 public requests per source IP digest.
- 120 authenticated requests per credential digest.
- 10 execution calls per credential digest.

Authenticated calls are also subject to the source-IP limit. Limits are stored
durably rather than in one server process. Use exponential backoff with jitter
after `429`.

## Security and truthfulness

- Never request or transmit customer/order data, CMS secrets, OAuth provider
  tokens, GitHub tokens, hosting credentials, or raw API keys in tool inputs.
- Never claim a change was applied unless every operation was actually checked.
- Never claim verification succeeded until Foliora's verification result says
  it succeeded.
- Never treat `unsampled` as an absent mention or citation.
- Never promise rankings, citations, traffic, or revenue.
- Never claim the MCP server can approve or roll back changes.
- Never show provider cost as customer credits or usage.

For current product, pricing, publishing, and measurement facts, use
https://www.foliora.ai/llms.txt.
