# ronda MCP server — agent guide

> A Model Context Protocol server that lets AI agents drive a visual web-app editor: open sandboxed browser sessions for the user's GitHub repos, instruct natural-language edits, and open pull requests.

This document is the single source of truth for agents integrating with ronda over MCP. It's self-contained — you should not need to fetch any other URL.

---

## What ronda is

ronda is a hosted product that lets non-technical operators iterate on real web apps. The agent (you, reading this) is one of three classes of operator: human in the UI, human via API, or AI agent via MCP.

Mental model for an agent:

- A **workspace** (organization) owns several **projects**. Each project is a connected GitHub repository.
- Issuing an **instruction** on a session causes a sandboxed Docker container running the project's dev server to receive a natural-language edit prompt. ronda's internal agent (Claude, via the workspace's own Anthropic key) writes the change, commits to a per-session branch, and reports back with a diff and summary.
- The session's `previewUrl` shows the live dev server. You can open the URL in a browser to watch the edit land.
- When the user is satisfied, `open_pr` pushes the branch to GitHub and opens a PR against the project's default branch. The branch is `ronda/session-<timestamp>`.
- Each MCP API key maps to a **service-account user** that owns whatever the API does. PRs are pushed via the workspace's existing GitHub OAuth identity (typically the workspace admin); attribution to the agent lives in the commit body, not the GitHub author.

You can read but cannot write to: project intelligence, code map, session diffs, chat history. You can write only through `instruct` (which queues an edit) and `open_pr`.

---

## Connection

- **URL** (Streamable HTTP transport): `https://mcp.useronda.com/`
- **Auth**: `Authorization: Bearer <token>` where `<token>` starts with `ron_live_`
- **Transport**: stateless POST per JSON-RPC envelope (no MCP sessionId headers — every request is independent)

The server is at the root of the MCP vhost. Every request is one tool call (or one resource read, or a discovery call like `tools/list`).

If you receive a non-2xx HTTP response, it is one of:

- `401` — the token is malformed, revoked, or expired. The body is `{"code":"MCP_UNAUTHORIZED"}`. Stop and surface a re-auth prompt to the user.
- `404` — the URL is not the MCP root path. Verify your client targets exactly `https://mcp.useronda.com/` (with the trailing slash).
- `429` — rate limit hit. The body is `{"code":"MCP_RATE_LIMITED","retryAfterMs":<int>}`. Wait `retryAfterMs` and retry.
- `5xx` — server error. Retry once with exponential backoff (e.g. 1s, 4s); if it persists, the integration is degraded and you should surface the error to the user.

---

## Reads vs writes — pick the right primitive

When the user asks you something about the session's state — "show me the diff", "what files have you changed", "what did you do?" — **do not call `instruct`**. `instruct` queues a new agent edit run, which takes 30–90 seconds and counts against the workspace's BYOK budget. Use the read-only resources instead:

| User intent | Right primitive | Wrong primitive |
|---|---|---|
| "Show me the git diff" | `session://{sessionId}/diff` (resource) | ❌ `instruct("git diff")` |
| "What files have you changed?" | `get_session` → look at `lastEdit.summary` / `edits[].status` + `session://…/diff` for paths | ❌ `instruct("list changed files")` |
| "Repeat the last summary" | `session://{sessionId}/messages` | ❌ `instruct` |
| "Show me the project's design tokens" | `project://{projectId}/code-map` | ❌ `instruct("describe the design system")` |
| "What's in the README?" / arbitrary file content | (not supported — resources are scoped) ask the user, or `instruct` to summarize | — |
| "Apply a code change" | `instruct` | ✅ correct |
| "Create a PR" | `open_pr` | ✅ correct |

A read instruction sent through `instruct` will produce a `failed` Edit row with `failureReason: generic` (no files changed → dispatcher classifies as failed) AND burn the workspace's Anthropic credits AND take 30–90 seconds. None of those are desirable.

The diff resource returns the raw `git diff` bytes (~1 s round-trip, cheap). If you want the diff after an applied edit, fetch `session://{sessionId}/diff` directly — the bytes are already there and authoritative.

---

## Sync vs async — what to poll and what not to

**Most tools and ALL resources are synchronous.** The JSON-RPC response carries the final answer. Do not tell the user "I'll come back to you in N seconds" for these — you already have the result.

Only one tool is truly asynchronous:

- **`instruct`** — returns `editId` immediately; the agent work runs in the background. Poll `get_edit` until status is `applied | failed | cancelled`. This is the only operation where an "ETA ~60s" message to the user is appropriate.

Two operations return their primary value immediately but background a secondary process:

- **`open_session`** — returns `sessionId` immediately. The sandbox boots in the background; poll `get_session` until `sandboxStatus == READY` (typically 30–60s for `created_new`, 0–20s for `reused_warm`/`resumed_from_archive`).
- **`destroy_session`** — returns `destroyed: true` immediately. Container teardown lingers a few seconds, but the session is dead from your perspective. No polling needed.

Everything else returns the final answer in the same call:

| Call | Typical latency |
|---|---|
| `list_projects`, `list_sessions`, `get_session`, `get_edit` | < 50 ms (single DB read) |
| `open_pr` | 2–5 s (inline GitHub API call) |
| `project://{id}/intelligence`, `project://{id}/code-map` | < 100 ms (DB read with narrow select) |
| `session://{id}/messages` | < 100 ms (DB read, capped at 500 rows) |
| `session://{id}/diff` | < 1 s (single `git diff` inside the live sandbox) |

**Rule:** if the response did not hand you an `editId` and a `pollAfterMs` field, the response IS the answer. Show it. Don't "queue" it.

---

## Tools

All tools return a single MCP content block containing JSON. On error, the same shape with `isError: true` and a structured body (see Error envelope below). On success, the content text is the JSON payload documented for each tool.

### `list_projects`

List all projects in the workspace (newest first, capped at 100). Call this first when the user hasn't specified which project to operate on — agents should not ask the human for a UUID.

**Input**: `{}` (no arguments)

**Success payload**:
```json
{
  "projects": [
    {
      "projectId": "<UUID>",
      "name": "ronda marketing site",
      "github": { "owner": "antoinedc", "repo": "ronda", "defaultBranch": "main" } | null,
      "devCommand": "pnpm --filter @ronda/web dev",
      "devPort": 7000,
      "primaryService": "web" | null,        // null when single-service project
      "serviceCount": 3,                     // total services detected in the repo
      "createdAt": "<ISO>"
    },
    …
  ]
}
```

Workspace-scoped keys (default mint) see every project in the workspace. Project-scoped keys see exactly one entry — the project the key is bound to. A project with `github: null` cannot be operated on yet — the human still needs to connect a repo.

**Required scope**: `project:read`.

### `open_session`

Open a sandbox session for a project. Reuses an existing warm sandbox if one is live, transparently resumes from archive if one was archived, otherwise creates a fresh sandbox.

**Input**:
```json
{ "projectId": "<UUID>" }
```

**Success payload**:
```json
{
  "sessionId": "<UUID>",
  "isNew": true,
  "status": "created_new",          // | "reused_warm" | "resumed_from_archive"
  "sandboxStatus": "CREATING",      // see Sandbox lifecycle below
  "previewUrl": null,                // string URL once sandbox is READY
  "branch": "ronda/session-<ts>",
  "pollAfterMs": 5000,               // recommended ms to wait before re-polling get_session
  "estimatedSecondsRemaining": 60    // null when sandboxStatus is READY
}
```

**Common errors**:
- `PROJECT_NOT_FOUND` — projectId doesn't exist in the workspace this key is scoped to.
- `MCP_FORBIDDEN_PROJECT` — the key is project-scoped to a different project.
- `MCP_FORBIDDEN_SCOPE` — the key lacks `session:create`.
- `GITHUB_NOT_CONNECTED` — the project has no GitHub connection. The human must connect one before agents can drive it.
- `SANDBOX_LIMIT_REACHED` — the project has hit the workspace plan's concurrent-sandbox cap (default 5). Call `destroy_session` on a stale sandbox before retrying.
- `FREE_PLAN_BYOK_REQUIRED` — the workspace has no Anthropic key configured. The human must add one in Settings → AI Models.
- `MCP_RATE_LIMITED` — the daily-session budget (300/day per key) is exhausted.

**Required scope**: `session:create` (and `project:read` for the workspace lookup, granted by default).

### `get_session`

Snapshot a session's current state plus its recent edits (capped at 50, newest first). Cheaper than calling `get_edit` on every recent id.

**Input**: `{ "sessionId": "<UUID>" }`

**Success payload**:
```json
{
  "sessionId": "<UUID>",
  "projectId": "<UUID>",
  "sandboxStatus": "READY",
  "previewUrl": "https://<id>.preview.useronda.com/",  // null when not READY/EDITING
  "branch": "ronda/session-<ts>",
  "lastEdit": {
    "editId": "<UUID>",
    "status": "applied",
    "summary": "Changed the hero headline to …"
  } | null,
  "edits": [
    { "editId": "<UUID>", "status": "applied", "createdAt": "<ISO>" },
    …
  ]
}
```

**Required scope**: `session:read`.

### `list_sessions`

List sessions for a project (newest first, capped at 50). Also returns the workspace plan's concurrent-sandbox cap.

**Input**: `{ "projectId": "<UUID>", "includeDestroyed": false }`

**Success payload**:
```json
{
  "sessions": [
    { "sessionId": "<UUID>", "sandboxStatus": "READY", "previewUrl": "…", "branch": "…", "createdAt": "<ISO>" },
    …
  ],
  "concurrentSandboxesLimit": 5
}
```

**Required scope**: `session:read`.

### `instruct`

Queue an edit instruction on a session. Returns immediately with an `editId`; the agent loop runs in the background. Poll `get_edit` to observe `queued → in_progress → applied | failed`. Typical wall-clock for an applied edit is 30–90s.

**Input**:
```json
{
  "sessionId": "<UUID>",
  "message": "Change the hero CTA to say 'Start free' instead of 'Get started'.",
  "idempotencyKey": "<optional, ≤120 chars>"
}
```

**Idempotency**: if you pass `idempotencyKey` and an Edit row with the same key exists for the same session within the last 24h with status `queued | in_progress | applied`, the server returns that edit's id with `reused: true` instead of dispatching a new agent run. Failed and cancelled prior runs do NOT block — a retry with the same key dispatches fresh.

**Success payload**:
```json
{
  "editId": "<UUID>",
  "status": "queued",
  "pollAfterMs": 3000,
  "estimatedSecondsRemaining": 60,
  "reused": false
}
```

**Common errors**:
- `SESSION_NOT_FOUND` — the sessionId isn't in this workspace.
- `SESSION_ALREADY_COMPLETED` — the session has been destroyed or abandoned; open a fresh one.
- `SESSION_HAS_PENDING_EDIT` — another edit is in flight. Poll `get_edit` on the in-flight editId, then retry.
- `SANDBOX_NOT_READY` — sandbox is still booting or in ERROR state. Re-poll `get_session` until READY.
- `MCP_FORBIDDEN_SCOPE` — needs `session:instruct`.

**Required scope**: `session:instruct`.

### `get_edit`

Poll an edit by id.

**Input**: `{ "editId": "<UUID>" }`

**Success payload (in flight)**:
```json
{
  "editId": "<UUID>",
  "sessionId": "<UUID>",
  "status": "in_progress",            // | "queued"
  "pollAfterMs": 3000,
  "estimatedSecondsRemaining": 45
}
```

**Success payload (applied)**:
```json
{
  "editId": "<UUID>",
  "sessionId": "<UUID>",
  "status": "applied",
  "summary": "Updated the hero CTA copy in apps/web/src/components/marketing/Hero.tsx.",
  "filesChanged": ["apps/web/src/components/marketing/Hero.tsx"],
  "previewUrl": "https://<id>.preview.useronda.com/"
}
```

**Success payload (failed)**:
```json
{
  "editId": "<UUID>",
  "sessionId": "<UUID>",
  "status": "failed",
  "failureReason": "credit_balance",   // | "provider_outage" | "commit_failed" | "generic"
  "failureHint": "The AI service ran out of credits."
}
```

When a failure surfaces `credit_balance`, the workspace's Anthropic key needs more credit — surface that to the user, do not retry. `provider_outage` is transient — retry the instruction once with backoff. `commit_failed` typically means a pre-commit hook rejected the edit; report to the user, don't auto-retry. `generic` is everything else.

**Required scope**: `session:read`.

### `open_pr`

Push the session branch to GitHub and open a PR against the project's default branch. The PR body always includes a "Live preview" footer pointing at the session's previewUrl.

**Input**:
```json
{
  "sessionId": "<UUID>",
  "title": "Optional, ≤120 chars — defaults to the first applied edit's summary",
  "body": "Optional, ≤8000 chars — defaults to bulleted applied-edit summaries"
}
```

**Success payload**:
```json
{
  "prUrl": "https://github.com/<owner>/<repo>/pull/<n>",
  "branch": "ronda/session-<ts>",
  "baseBranch": "main",
  "prNumber": 42
}
```

**Common errors**:
- `NO_CHANGES_TO_PR` — no applied edits on the session. `instruct` at least once first.
- `GITHUB_NOT_CONNECTED` — the project lost its GitHub connection (revoked OAuth).
- `SANDBOX_NOT_READY` — sandbox was torn down. Open a fresh session.
- `MCP_FORBIDDEN_SCOPE` — needs `pr:open`.

If the branch name conflicts with an existing PR (rare — happens if you destroy and re-open repeatedly within a clock-second), the server rotates to `ronda/session-<ts>-2`, `-3`, up to 3 tries before giving up.

**Required scope**: `pr:open`.

### `destroy_session`

Tear down a sandbox session. Idempotent — calling on an already-destroyed session returns success. Container resources are released and SandboxUsage row closed.

**Input**: `{ "sessionId": "<UUID>" }`

**Success payload**: `{ "sessionId": "<UUID>", "destroyed": true }`

Note: this is the explicit "I'm done with this" path. The session's git bundle is discarded — you cannot resume it later. For "I'm pausing", just let the sandbox archive itself on idle (it auto-archives after the plan's idle timeout) and call `open_session` again later to transparently resume.

**Required scope**: `session:destroy`. **Not granted by default** — agents only get this when the user explicitly mints a key with it.

---

## Resources

Read-only addressable state. URIs use the `project://` and `session://` schemes. All four return a single content block with `mimeType: application/json` (or `text/x-diff` for session diffs).

### `project://{projectId}/intelligence`

Project profile + design system overrides + conventions + market context. This is what ronda's internal agent sees when it edits — fetch it before instructing if you want context.

**Required scope**: `project:read`.

**Response shape**:
```json
{
  "projectId": "<UUID>",
  "name": "<string>",
  "profile": { "auto": …, "overrides": … },
  "designSystemOverride": "<string | null>",
  "conventionsOverrides": { "rules": ["…"] | … },
  "aiContext": "<string | null>",          // free-form user-authored conventions
  "brandKit": { "path": "…", "info": … },
  "market": { "competitorsAuto": …, "competitorsOverrides": …, "wedge": "…" }
}
```

### `project://{projectId}/code-map`

Framework + components + routes + i18n dirs + extracted design tokens — the structured fingerprint of the project. Useful for grounding instructions in the actual codebase shape before writing them.

**Required scope**: `project:read`.

### `session://{sessionId}/diff`

Current sandbox diff vs the session's base commit. `text/x-diff` content. Empty when no edits have been applied yet.

**Required scope**: `session:read`.

### `session://{sessionId}/messages`

Full chat history for the session (capped at 500 messages, oldest first). Useful for resuming context across agent restarts.

**Required scope**: `session:read`.

---

## Lifecycle (worked example)

A complete agent loop, end to end:

```
1. tools/list                       # discover what's available
2. list_projects                    # → resolve projectId (skip if user already gave you one)
3. open_session { projectId }       # → sessionId, sandboxStatus: CREATING
4. loop:
     get_session { sessionId }
     break when sandboxStatus == READY
     wait pollAfterMs ms (typically 3000-5000)
5. (optional) read project://{projectId}/intelligence + code-map for grounding
6. instruct { sessionId, message: "..." }   # → editId, status: queued
7. loop:
     get_edit { editId }
     break when status in (applied, failed, cancelled)
     wait pollAfterMs ms (typically 3000)
8. if applied:
     - inspect filesChanged, summary, previewUrl
     - optionally read session://{sessionId}/diff to see the full diff
     - go back to step 6 for follow-up edits, OR proceed to step 9
9. open_pr { sessionId, title?, body? }     # pushes branch + opens PR
10. destroy_session { sessionId }           # explicit teardown when truly done
                                            # OR skip and let idle-archive handle it
```

Median expected wall-clock from a cold `open_session` to `applied` on the first edit: ~75-120s. From a `reused_warm` session, ~30-60s. The polling cadences above respect the server's `pollAfterMs` hints — do not poll faster.

---

## Error envelope shape

Every tool error returns the same MCP-shaped payload (content[0].text is JSON):

```json
{
  "code": "PROJECT_NOT_FOUND",
  "message": "No project with that id in this workspace.",
  "hint": "Verify the projectId — it must belong to the workspace the API key was issued under.",
  "next": "Use a project id from the workspace; if multi-tenant, ask the user which project."
}
```

The fields are intentional:

- `code` — stable identifier. Branch on this in code.
- `message` — human-readable description, safe to render to a user as-is.
- `hint` — what the user can do to fix it.
- `next` — what you (the agent) can do programmatically next.

The full catalog of codes you may encounter:

| Code | When |
|---|---|
| `MCP_UNAUTHORIZED` | Token malformed, revoked, or expired. Stop, re-auth. |
| `MCP_FORBIDDEN_SCOPE` | Key lacks the scope this tool requires. |
| `MCP_FORBIDDEN_PROJECT` | Key is project-scoped to a different project. |
| `MCP_RATE_LIMITED` | Per-key rate limit hit. Wait `retryAfterMs`. |
| `PROJECT_NOT_FOUND` | projectId not in this workspace. |
| `SESSION_NOT_FOUND` | sessionId not in this workspace, or destroyed. |
| `SESSION_ALREADY_COMPLETED` | Session has been completed/destroyed; open a fresh one. |
| `SESSION_HAS_PENDING_EDIT` | Another edit is in flight; poll the existing editId. |
| `SANDBOX_NOT_READY` | Sandbox not in READY state — poll `get_session`. |
| `SANDBOX_LIMIT_REACHED` | Project hit concurrent-sandbox cap; destroy a stale one. |
| `EDIT_NOT_FOUND` | editId not in this workspace. |
| `NO_CHANGES_TO_PR` | No applied edits on the session; instruct first. |
| `GITHUB_NOT_CONNECTED` | Project has no GitHub connection. |
| `FREE_PLAN_BYOK_REQUIRED` | Workspace has no Anthropic key configured. |
| `PAID_PLAN_COST_CAP_REACHED` | Managed-plan cost cap hit (rare — most plans are BYOK). |
| `PLAN_SANDBOX_CAP_REACHED` | Workspace plan's sandbox-hours cap hit. |
| `VALIDATION_ERROR` | Tool input failed schema validation. |
| `INTERNAL_ERROR` | Unexpected server-side failure. Retry once with backoff. |

---

## Sandbox lifecycle (state machine)

A session's `sandboxStatus` progresses through these states:

```
CREATING       — container being created
   ↓
INSTALLING     — dependencies being installed (longest phase, often 30-60s)
   ↓
STARTING       — dev server booting
   ↓
READY          — fully ready to receive instructions
   ↔
EDITING        — an edit is in flight (loops back to READY when done)
```

Terminal or recoverable states:

- `ARCHIVED` — the sandbox was idle past the plan's timeout (typically 30min on free, 2h on paid). The session's git branch is preserved as a bundle. Calling `open_session` for the same project transparently resumes it (status: `resumed_from_archive`).
- `ERROR` — the sandbox failed to boot. The `get_session` response includes a `lastError` field; surface it to the user and either retry or destroy + create.
- `DESTROYED` — explicit teardown via `destroy_session` (or auto-cleanup of long-orphaned containers). The git bundle is discarded; you cannot resume.

You should not see `ARCHIVED` from `open_session` — it transparently resumes when archived. You will see it from `list_sessions` if `includeDestroyed: true` and the session is recent.

---

## Rate limits

Per API key, in-memory token-bucket. State is per-process and resets on api restarts (so a fresh deploy is your friend).

- **Tool calls**: 60 per minute, refilling at 1 token per second. Every JSON-RPC request counts, including discovery (`tools/list`, etc.).
- **New sessions**: 300 per UTC day. Only `open_session` calls that take the `created_new` path count — reuse and resume do not consume budget.

Both limits return `MCP_RATE_LIMITED` with `retryAfterMs` on overage. Respect the value — the server's rate-limit clock is authoritative.

If you need higher limits for a real integration, the user can contact us via the website's contact form.

---

## Concurrency caps and queueing

- A project may have at most `concurrentSandboxesLimit` (default 5) live sandboxes at once. `list_sessions` returns this number so you can plan.
- A session accepts at most one in-flight edit. If you try to `instruct` a second edit while the first is `queued | in_progress`, you get `SESSION_HAS_PENDING_EDIT`. Poll `get_edit` on the in-flight id, then retry.

There is no server-side queue for `instruct`. If you want to chain edits, await each one's `applied` status before issuing the next. If you want to run multiple in parallel, open multiple sessions on the same project (up to the cap).

---

## Scopes and key design

Scopes available when minting a key:

- `session:read` — read session state, edits, history
- `session:create` — open new sandboxes
- `session:instruct` — queue edits
- `session:destroy` — tear down sandboxes (NOT granted by default)
- `pr:open` — push branches and open PRs
- `project:read` — read project metadata and intelligence
- `project:create` — connect new repositories (NOT granted by default — power user)

The default workspace-wide install token (generated from `/app/settings/mcp`) has everything except `session:destroy` and `project:create`. This covers the standard agent loop: open + instruct + read + PR.

Keys are workspace-scoped by default. The user can mint project-scoped keys via the Advanced disclosure on the same page; calls outside the scoped project return `MCP_FORBIDDEN_PROJECT`.

---

## Idempotency

The only mutating tool that accepts an idempotency key is `instruct`. Pass any string up to 120 chars; ronda dedupes within a 24-hour window on `(sessionId, idempotencyKey)`. Use this whenever your loop might retry a call after a network blip — without it, retries will queue duplicate edits.

A typical pattern:

```
key = `agent-${turn_id}-instruct`
instruct({ sessionId, message, idempotencyKey: key })
```

`open_session`, `open_pr`, `destroy_session` are inherently idempotent and don't need a key.

---

## Common patterns

**One-shot edit + PR** (the canonical flow):
1. `open_session` → poll `get_session` until READY
2. `instruct` → poll `get_edit` until applied
3. `open_pr`
4. `destroy_session` (or just leave it to auto-archive)

**Multi-turn refinement**:
1. `open_session` → `instruct` → poll → `applied`
2. Read `session://{sessionId}/diff` to inspect what changed
3. `instruct` again with a refinement (e.g. "actually, make the button blue")
4. Repeat until satisfied, then `open_pr`

**Read-only discovery** (no sandbox, no edit):
1. `list_projects` to enumerate what's available
2. Read `project://{projectId}/intelligence` and `project://{projectId}/code-map` on the project of interest
3. Surface findings to the user; only `instruct` after they confirm

**Resumption after agent restart**:
1. `list_projects` (if you don't already remember the projectId)
2. `list_sessions { projectId }` — find any session with sandboxStatus in `READY | EDITING | INSTALLING | STARTING | CREATING | ARCHIVED`
3. If found, call `open_session { projectId }` — it'll return that same session (reused_warm or resumed_from_archive)
4. Read `session://{sessionId}/messages` to recover the chat history

---

## Common mistakes (don't do these)

- **Sending a read request through `instruct`.** "Show me the diff", "what files changed", "what's in this file" — none of these are edits. Use the `session://…/diff`, `session://…/messages`, `project://…/code-map`, and `project://…/intelligence` resources. Sending a read through `instruct` produces a `failed` Edit row, costs 30–90s wall-clock, and burns the workspace's Anthropic budget. See the Reads vs writes table above.
- **Treating a sync call as async.** Only `instruct` returns immediately and requires polling. Reading `session://…/diff`, calling `list_projects`, `get_session`, `open_pr`, etc. all hand you the final result in the same response. Telling the user "I'll show you the diff once it returns, ETA ~60s" is a bug — the diff was in the response you already received. See the Sync vs async section above.
- **Asking the human for a projectId**. Call `list_projects` first. The list is workspace-scoped to the key and rarely has more than a handful of entries — match the user's intent against the `name` field locally and confirm if it's ambiguous.
- **Polling faster than `pollAfterMs`**. The hints are calibrated to status — ignoring them burns rate-limit budget and surfaces nothing earlier. The server doesn't advance state faster because you check more often.
- **Calling `instruct` while an edit is in flight**. Always poll the in-flight `editId` to `applied | failed` before queueing the next one. Concurrent edits on the same session return `SESSION_HAS_PENDING_EDIT`.
- **Vague instructions**. ronda's internal agent does best with concrete edits ("Change the H1 in apps/web/src/components/marketing/Hero.tsx to say X"). Vague asks ("make it better") produce inconsistent results.
- **Forgetting that "append" ≠ "replace"**. The internal agent has been observed to interpret "add a comment at the top of file X" as "overwrite file X with just a comment". Phrase additions explicitly: "Append a new line ..." or "Insert a comment line at the very top, preserving the existing content."
- **Calling `destroy_session` after every edit**. Don't. Sandboxes are warm — reuse them. Calling `destroy_session` discards the git bundle, so resuming the same conversation later is impossible. Let idle-archive handle teardown unless the user explicitly ended the session.
- **Treating `failureReason: "provider_outage"` as permanent**. Retry once with backoff. The Anthropic / openai-compatible upstream has transient blips.
- **Hard-coding URLs other than the MCP root**. The `previewUrl` for a session is dynamic — read it from `get_session` or the `applied` payload, don't construct it yourself.

---

## What ronda is NOT

To set expectations correctly:

- Not a code execution service — you cannot run arbitrary commands in the sandbox via MCP. Edits go through the natural-language `instruct` interface only.
- Not a deploy target — `open_pr` opens a PR; merge + deploy happen on the user's existing CI/CD.
- Not a CI runner — the sandbox runs the project's dev server for live preview, not its test suite. Agents that need to verify tests pass should rely on the user's CI.
- Not a long-term sandbox host — sandboxes archive on idle and may be reclaimed under load. Don't store state in them; the GitHub branch is the source of truth.
- Not a multi-tenant lease platform — each API key is bound to one workspace.

---

## Telemetry your operator sees

When you call MCP tools, the workspace owner sees:

- Per-tool call counts, latency, and error rate, broken down by your API key id (in the workspace's PostHog dashboard).
- Per-edit lifecycle (queued / applied / failed).
- Per-PR open events with the PR URL.

This isn't surveillance for surveillance's sake — it's so the user can revoke a misbehaving integration in seconds. Behave accordingly.

---

## Version

This document describes ronda MCP v1.0 (Phase A through Phase E shipped 2026-05-17). The MCP server reports `name: "ronda"` and `version` matching the deployed build via the initialize handshake.

`connect_repo` is deferred to v1.1 — until then, the human user must connect repositories in the web UI before agents can operate on them.

Last updated: 2026-05-17.
