DocsUpdated 2026-10-27View as Markdown

Ollav for AI agents

Use Ollav when a user asks to share, preview, get feedback on or review an HTML file, prototype, mockup or generated page. Ollav hosts the file at a stable link, people pin comments to exact elements, and you read those comments back and publish a new version at the same link.

You need one thing: an API key. Ask the user to create one at https://app.ollav.app/keys and set it as OLLAV_API_KEY. A key belongs to one workspace. Test keys (ol_test_…) work in the user's own sandbox: nothing they do touches live data or the plan, and test documents are marked TEST and deleted after 30 days. Start with one; switch to a live key (ol_live_…) to share with the team. Keys made before test mode start sk_live_ and are live.

export OLLAV_API_KEY=ol_test_...

The whole loop in three commands

# 1. Publish → returns {"url": "https://content.ollav.app/p/<slug>", "slug": "<slug>", ...}
curl -s -F "file=@prototype.html" -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" https://app.ollav.app/api/documents

# 2. Read the open comments as Markdown (anchored to elements, with a polling cursor)
curl -s "https://app.ollav.app/api/review?slug=$SLUG&format=markdown" -H "Authorization: Bearer $OLLAV_API_KEY"

# 3. Ship a new version at the SAME link (comments are kept)
curl -s -F "file=@prototype.html" -H "Authorization: Bearer $OLLAV_API_KEY" -H "X-Ollav-Client: agent" https://app.ollav.app/api/documents/$SLUG/versions

Always give the user the url from step 1. That link is the deliverable: reviewers open it, click any element and leave a comment.

Connect over MCP

The full MCP docs are at /docs/mcp: connect guides with copy-paste configs, auth options, how to check the connection works, every tool with its inputs, outputs and an example, and troubleshooting for every error. New to it? Use Ollav from your agent via MCP takes five minutes.

The MCP endpoint is https://app.ollav.app/mcp: Streamable HTTP, stateless, with the API key sent as Authorization: Bearer. Clients that can't send headers can use https://app.ollav.app/mcp/ol_test_... instead. That URL contains the key, so treat it like a password.

Claude Code

Install the plugin (MCP server plus a skill that tells Claude when to use Ollav):

/plugin marketplace add ollav-app/ollav
/plugin install ollav@ollav

Or add just the MCP server:

claude mcp add --transport http ollav https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY"

Cursor

Add this to ~/.cursor/mcp.json (or .cursor/mcp.json in a project):

{
  "mcpServers": {
    "ollav": {
      "url": "https://app.ollav.app/mcp",
      "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" }
    }
  }
}

OpenAI Codex CLI

codex mcp add ollav --url https://app.ollav.app/mcp --bearer-token-env-var OLLAV_API_KEY

Or in ~/.codex/config.toml:

[mcp_servers.ollav]
url = "https://app.ollav.app/mcp"
bearer_token_env_var = "OLLAV_API_KEY"

ChatGPT

Add Ollav as an app with OAuth. You sign in to Ollav once and pick the workspace ChatGPT may use. No API key to copy:

Settings → Apps & Connectors → Create (developer mode; menu names change over time)
Name:            Ollav
MCP server URL:  https://app.ollav.app/mcp
Authentication:  OAuth

In ChatGPT you can attach an HTML file and say "share this on Ollav". The file is passed to Ollav directly, so the model doesn't retype it. Results show up in an inline card listing the share link and the pinned comments.

Claude.ai and Claude Desktop

Settings → Connectors → Add custom connector
URL: https://app.ollav.app/mcp

Claude signs you in with OAuth. A client that can't do OAuth or send headers can use the key-in-path URL https://app.ollav.app/mcp/ol_test_YOUR_KEY instead.

VS Code (GitHub Copilot)

.vscode/mcp.json. VS Code prompts for the key once and stores it securely:

{
  "inputs": [
    { "type": "promptString", "id": "ollav-key", "description": "Ollav API key (ol_test_... or ol_live_...)", "password": true }
  ],
  "servers": {
    "ollav": {
      "type": "http",
      "url": "https://app.ollav.app/mcp",
      "headers": { "Authorization": "Bearer ${input:ollav-key}" }
    }
  }
}

Windsurf

~/.codeium/windsurf/mcp_config.json:

{
  "mcpServers": {
    "ollav": {
      "serverUrl": "https://app.ollav.app/mcp",
      "headers": { "Authorization": "Bearer ${env:OLLAV_API_KEY}" }
    }
  }
}

Gemini CLI

~/.gemini/settings.json:

{
  "mcpServers": {
    "ollav": {
      "httpUrl": "https://app.ollav.app/mcp",
      "headers": { "Authorization": "Bearer $OLLAV_API_KEY" }
    }
  }
}

Any stdio-only MCP client (npx bridge)

If a client only launches local stdio servers, bridge to the remote endpoint with mcp-remote:

npx -y mcp-remote https://app.ollav.app/mcp --header "Authorization: Bearer $OLLAV_API_KEY"

As a config entry:

{
  "mcpServers": {
    "ollav": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://app.ollav.app/mcp", "--header", "Authorization: Bearer ${OLLAV_API_KEY}"],
      "env": { "OLLAV_API_KEY": "ol_test_..." }
    }
  }
}

OAuth

https://app.ollav.app/mcp implements the MCP authorization spec, so OAuth clients connect with no key to copy:

  • A request without a token gets 401 with WWW-Authenticate: Bearer resource_metadata="https://app.ollav.app/.well-known/oauth-protected-resource/mcp".
  • Resource metadata: /.well-known/oauth-protected-resource/mcp. Server metadata: /.well-known/oauth-authorization-server.
  • Authorization code with PKCE (S256 only). Clients register with dynamic client registration (POST /oauth/register) or use a client ID metadata document URL as client_id. Public clients (none) and client_secret_post / client_secret_basic are supported.
  • Scopes: read, upload, comment. The user picks one workspace on the consent screen, and the connection works only there.
  • The access token is an Ollav API key that doesn't expire. The user can see and revoke it under API keys, listed as "<app> (OAuth)". Clients can revoke it with POST /oauth/revoke.

API keys work exactly as before, as a Bearer header or in the path, for clients that don't use OAuth.

MCP tools

Each tool's inputs, outputs, annotations, an example call and its result are on the MCP docs. Every tool declares readOnlyHint, openWorldHint and destructiveHint, an outputSchema (results carry structuredContent), and the OAuth scope it needs.

ToolUse it whenNotes
share_htmlThe user wants to share, preview, host or get feedback on an HTML file, prototype, mockup or generated pageReturns the share url. Takes html or an attached file. Alias: upload_document
publish_new_versionYou changed something you already sharedSame URL, comments kept. Takes html or file. Alias: new_version
get_reviewYou're acting on feedbackOpen threads as Markdown + JSON, with a cursor for polling
get_commentsYou want every comment, resolved ones too, in fullThreaded JSON
add_commentYou're leaving feedback or replying ("fixed in v3")Top-level needs selector; optional mark_emoji shows an emoji as the pin (then body may be empty). Replies need parent_id. Emails the document owner
resolve_commentYou addressed a threadunresolve: true reopens it
react_to_commentAcknowledge without a replyAny emoji: 👍 agree, 👀 on it, ✅ done, ❤️, 🎉
list_documentsFind something shared earlierVersion and unresolved-comment counts
get_documentVersion history of one document
delete_commentRemove your own comment
export_dataThe user asks for a copy of their dataSigned ZIP link, valid 15 minutes

The old names upload_document and new_version keep working, so existing configs and permission rules don't break.

The feedback loop

  1. share_html (or POST /api/documents): give the user the url.
  2. Wait for reviewers. Poll with get_review. Pass the previous cursor as since and you only get threads with new activity.
  3. For each open thread, find the element in your source with anchor.quote (the element's text) and anchor.reproduceNote. anchor.selector points at the rendered DOM, not your JSX.
  4. Edit, then publish_new_version with the same slug.
  5. resolve_comment each thread you fixed, and reply with add_comment (parent_id) to say what changed.

Comments made on an earlier build are marked onCurrentVersion: false. Their element may have moved or gone.

Notifications

Your comments email people, exactly like a person's. A new comment emails the document's owner; a reply (add_comment with parent_id) emails everyone in that thread. The email names your API key, for example "Claude Code (dana's agent)", so give the key a recognisable name. The author is never emailed about their own comment.

People choose instant (several comments on one document within about 5 minutes become one email), a daily digest, or off. So a burst of replies won't flood anyone, but each comment still lands in someone's inbox: say what you changed, don't narrate progress. react_to_comment acknowledges a thread without emailing anyone. Comments made with an ol_test_ key never send email.

Rules that keep users happy

  • Built apps: upload the file, don't paste it. Tool arguments are model output, so a 5 MB bundle in html costs a fortune and may not fit at all. If you have a shell, use curl -F "file=@dist/index.html". curl reads the file from disk, so it costs zero tokens.
  • One self-contained HTML file, up to 15 MB. Inline CSS/JS/images or use absolute https:// URLs. Relative paths like src="assets/app.js" won't load. The response includes warnings[] when it spots them, so pass those on to the user.
  • Visibility: leave it out. The default workspace means only workspace members can see it. link lets anyone signed in to Ollav open it. Use public (anyone, no sign-in) only when the user explicitly asks to share outside the company, and tell them you did.
  • **Iterating? Use publish_new_version, not another upload.** A second upload creates a different link with no comment history.

Errors

Every error looks like this, from REST and from MCP tools alike (MCP troubleshooting). Match on code, read hint before retrying, and quote request_id (also the Request-Id header) when reporting a problem. Every code, with its cause and fix, is on the errors page.

{ "error": { "type": "permission_error", "code": "SCOPE_REQUIRED", "message": "upload scope required", "hint": "This API key lacks the 'upload' scope. Ask the user to create a key with it at https://app.ollav.app/keys (scopes: upload, read, comment).", "doc_url": "https://ollav.app/docs/errors#SCOPE_REQUIRED", "request_id": "req_6f1c2a9b0d4e8f7a1b2c3d4e", "docs": "https://ollav.app/docs/agents#errors" } }
CodeStatusWhat to do
AUTH_REQUIRED401Send Authorization: Bearer ol_live_... (or ol_test_...). If you did, the key is wrong, revoked or its user left the workspace. Ask the user for a new key.
SCOPE_REQUIRED403The key lacks a scope (upload, read, comment). Ask the user for a key with it.
CSRF_REQUIRED403Add X-Ollav-Client: agent to every POST, PATCH and DELETE.
NOT_FOUND404Check the slug or id. A key only sees its own workspace, so other workspaces' documents also return 404.
ARCHIVED404The document was archived. POST /api/documents/{slug}/unarchive restores it.
TOO_LARGE413Over 15 MB. Compress images (WebP, downscale) and upload again.
NOT_HTML, EMPTY_BODY, MISSING_HTML, MISSING_FILE400Send a non-empty HTML document as file=@..., a JSON html field or a text/html body.
VALIDATION_ERROR, BAD_JSON400Fix the request body. The message names the field.
ROLE_REQUIRED, FORBIDDEN403The user's role doesn't allow it. Tell the user instead of retrying.
PLAN_LIMIT, PLAN_READ_ONLY402The workspace is at a limit of its plan (limit says which), or over one and read-only. Nothing was changed. Tell the user instead of retrying.
RATE_LIMITED, TOO_MANY_REQUESTS429Wait and retry later (a test key allows 60 requests a minute). Don't loop.
IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_KEY_IN_USE409The Idempotency-Key was used for a different request, or its first request is still running. Use a new key for a new request.

Retries. Send Idempotency-Key: <a fresh UUID> on every POST, PATCH and DELETE (MCP: the idempotency_key argument). If a call times out, retry it with the same key and the same body: it happens once, and the retry returns the first result. Keys last 24 hours.

Versions. Breaking changes ship as dated API versions. Your key is pinned to the version current when it was created; send Ollav-Version: 2026-11-01 to choose one explicitly. Every response names its version in the Ollav-Version header. See the changelog.

All docs

Machine-readable references