MCP reference
The endpoint, sign-in, the four tools, limits and errors.
This page is for people building an MCP client or wiring up an agent by hand. If you only want to connect Claude Code, Cursor or another agent, use Coding agents.
Endpoint
https://ramble.sh/mcpIt is a remote MCP server on the streamable HTTP transport, named ramble. There are no sessions: each request stands alone. A browser page on another site can't call it. A request whose Origin header isn't ramble.sh itself gets a 403.
When a client connects, the server sends a short instructions text. It holds your counts (rambles with findings no agent has acted on, open record requests) and your autonomy setting. It never holds a title, a quote or a note.
Signing in
There are two ways to authenticate. Both end as Authorization: Bearer … on each request.
OAuth
This is what Claude Code, Codex, Gemini CLI, Cursor and VS Code use. A client that follows the MCP authorization flow needs nothing but the URL.
- Call
/mcpwith no token. It answers 401 with aWWW-Authenticateheader carrying aresource_metadataaddress. - Read the discovery documents.
/.well-known/oauth-protected-resourcenames the resource and the authorization server./.well-known/oauth-authorization-serverlists the endpoints below. - Register the client by dynamic client registration (RFC 7591) at
/api/oauth/register. Clients are public: the onlytoken_endpoint_auth_methodaccepted isnone. A redirect URI may be a loopback address (http://127.0.0.1,http://localhostorhttp://[::1], any port), anhttpsaddress, or a private-use scheme followed by://. - Authorize at
/oauth/authorizewith the authorization code flow and PKCE. OnlyS256is accepted. Ask for the scopes you need. The person sees a consent page with your client's name marked "unverified name", and presses Connect. The redirect carries anissparameter. - Exchange the code at
/api/oauth/token(grant_type=authorization_code). - Refresh at the same endpoint (
grant_type=refresh_token). Refresh tokens rotate: every refresh returns a new one. Using an old one a second time is treated as theft and revokes that client's tokens for the person. - Revoke at
/api/oauth/revoke(RFC 7009).
An access token lasts 24 hours. A refresh token lasts 90 days since it was last used. Any failure of a code or a refresh token answers with the same invalid_grant error, so the response doesn't tell you which part was wrong.
A personal token
For an agent that can't sign in through a browser. The person makes one under Connected agents on Your rambles and gives it to you. It is shown once, carries all three scopes and doesn't expire until it is revoked. Send it as Authorization: Bearer TOKEN.
What a bad request gets
- No token, or one that isn't valid or has expired: 401
unauthorized, with theWWW-Authenticateheader above. - A token without
rambles:read: 403insufficient_scope.
Scopes
| Scope | Lets the client |
|---|---|
rambles:read | Use ramble_find, and ramble_read on a ramble or a finding. Every connection needs it to reach /mcp. |
rambles:annotate | Use ramble_work: claim findings, set statuses, comment, set a project, set the autonomy setting (back to plan_first only). |
rambles:request | Use ramble_ask to ask the person to record and to cancel, and ramble_read with a requestId to wait. |
All four tools are always listed. The scope is checked for each call, and for ramble_read by what you ask it: a ramble or a finding needs rambles:read, a request needs rambles:request. A call whose scope the token lacks answers with an insufficient_scope tool error rather than an HTTP error. No scope covers uploading, recording, deleting or editing findings. Agents can't do those.
The four tools
Every tool definition a server lists is context an agent pays for in every session, and Ramble is usually one of several servers it has connected. So related operations share a tool, and one short argument picks the operation: view on ramble_find, action on ramble_work and ramble_ask. Reading and writing are separate tools, so a client can treat the two reading tools as read-only (readOnlyHint) and ask before the two writing tools.
| Tool | Reads or writes | What it does |
|---|---|---|
ramble_find | reads | List rambles and projects, and say who you are connected as. |
ramble_read | reads | Read one ramble or one finding, with screenshots, or wait for a requested ramble. |
ramble_work | writes | Claim findings, set statuses, comment, set a project, set the autonomy setting. |
ramble_ask | writes | Ask the person to record a short check, or cancel the request. |
The schemas are flat on purpose, so the server, not the schema, says what an action still needs: a call that leaves something out answers invalid_argument with the missing names in missing, and costs nothing against the write limit.
ramble_find
Scope rambles:read. Orients you and finds things. The argument view picks what you get:
rambles(the default) lists the account's rambles, newest first. Arguments, all optional: -limit: 1 to 100, default 25. -since: an ISO 8601 date or timestamp. Rambles recorded at or after it. -state:ready,processing,failedorrecording. -project: a project key such asgithub.com/owner/repo, or an id. -unassigned: only rambles with no project. -unactioned: only ready rambles with findings no agent has touched. This is the inbox. -query: up to 200 characters, matched against the title, the app name, the request summary and finding titles.
Each ramble has id, createdAt, state, durationMs, title, appName, project, projectSource, projectCandidates, findingCount, agentSummary, claimedBy and requestId.
projectslists the account's projects:id,key,name,repoUrl,hintsandrambleCount.mesays who the connection is:mode, theemail, thescopes, theautonomysetting,autonomySource(where the setting comes from:user,defaultorenv), theserveraddress, and two counts:unactioned(rambles with findings no agent has acted on) andopenRequests. Call it first.
ramble_read
Reads one ramble, one finding, or waits on a request. Pass id, or requestId, not both.
A ramble (id, scope rambles:read) comes back as agent markdown: the recorder's words with m:ss times, the findings and the transcript. For the owner it adds what agents have done with it. Arguments:
id: the 12-character ramble id.frames:none(default),heroorall.heroreturns each finding's main screenshot as an image.allreturns every screenshot of every finding. Images come only when the ramble isready.maxImageBytes: 0 to 3000000, default 1500000. The budget for raw screenshot bytes.
structuredContent carries state, title, appName, durationMs, project, findingCount, claimedBy, enriching, enrichingTasks and a findings array. While enriching is true Ramble is still checking screenshots and labels: caption, elementName and bbox are null on the findings listed in enrichingTasks. Read again for the final values. Each finding has id, position (the number the markdown prints as its work item), kind, title, quote, t0Ms, t1Ms, attentionOrder, elementName, caption, bbox, bboxFromDiff, screenshots, and the agent fields agentStatus, agentStatusUrl, agentStatusNote, agentStatusActor, agentStatusAt and claim. These are summaries. They leave out description, expected, actual, reproSteps and the confidence fields: the markdown text carries the recorder's words and the reproduction notes, and a finding read (id and findingId, below) returns every field.
A finding (id and findingId, scope rambles:read) comes back with every field of the finding (description, expected, actual, reproSteps, confidenceScore, confidenceBand, needsVisual, attentionOrder and the ones in the summary), its agent status, its live claim and its screenshots as images. Use it for a screenshot the ramble read left out for size. frames is hero by default, or all or none. The bounding box is [ymin, xmin, ymax, xmax] on a 0 to 1000 scale. When bboxFromDiff is true, it marks where the screen changed, not an identified element.
A wait (requestId, scope rambles:request) waits for the ramble that answers a request made with ramble_ask. Arguments: requestId; cursor, the value the last call returned; and timeoutSeconds, 5 to 40, default 40. It returns as soon as the status changes, a new provisional finding appears or the status is final, or when the time is up. Call it again until the status is ready, failed, cancelled or expired, then read the rambleId with id.
The answer has status (open, recording, processing, ready, failed, cancelled or expired), rambleId, cursor, elapsedMs, requestAgeMs and provisional. Items in provisional are findings Ramble has proposed while the person is still talking. They carry provisional: true. They are previews: never act on them. If the ramble is deleted while you wait, discarded is true and the status is cancelled. If the client disconnects, the wait stops. If you send a progress token, the server sends a progress notification about every 10 seconds.
ramble_work
Scope rambles:annotate. Records your work on a ramble for the owner and other sessions. The argument action picks the operation, and each action needs its own arguments:
action | Arguments | What it does |
|---|---|---|
claim | rambleId, actor; findingIds (up to 100, or leave it out to claim the whole ramble), ttlMinutes (5 to 240, default 30) | Takes a lease on findings so another session doesn't work on them. Returns leaseId, expiresAt, claimed and renewed. If another session holds some of them, the answer is a held error with heldBy naming who and until when. Claiming again with the same connection and actor renews your own lease. |
renew | leaseId | Extends a lease this connection holds. Call it about every 20 minutes while you work. A lease that has ended is a not_found_or_expired error. |
release | leaseId | Gives a lease up. |
status | rambleId, findingId, status (planned, in_progress, pr_opened, fixed or wont_fix), actor; url (an https link, up to 500 characters), note (up to 1000 characters) | Records where a finding stands. It needs a live claim held by this connection. Without one the answer is not_claimed. |
comment | rambleId, note (1 to 2000 characters), actor; findingId | Leaves a note on a ramble, or on one finding. It needs no claim. |
project | rambleId, project: an object with key and optionally name and repoUrl, or null to clear it; actor is optional | Says which project a ramble is about. The key is normalised, and the project is created on first use. If the owner chose a project, it is kept and the answer says kept: true. |
autonomy | autonomy | From an agent connection only plan_first is accepted. act is refused with a not_available error: the person switches to act themselves, in the Connected agents card on Your rambles. Don't retry, and don't look for another credential. Do it only when the person asks you to in the conversation. |
The actor is who is writing: your agent name, the repo folder and the local time. Two sessions on one connection tell each other apart by it.
ramble_ask
Scope rambles:request. Asks the person to record a short ramble that checks something, or withdraws the request. The argument action is request or cancel.
request arguments:
brief: 1 to 2000 characters. What should now be true and how to try it.actor.project: a project key, optional.findingIds: up to 50 findings the recording should check. They must be findings on this account, or the answer isunknown_findings.context:repo,branch,commit,urls(up to 10) andchangedFiles(up to 50), all optional.expiresInMinutes: 5 to 240, default 60.
It returns requestId, deepLink (ramble://record?request=ID, for Ramble for Mac), webUrl (https://ramble.sh/?request=ID), expiresAt and howToOpen. Open exactly one of the two links for the person, then wait with ramble_read and the requestId. An account can have 20 open requests at a time.
cancel takes requestId and withdraws a request nobody has started answering. Otherwise the answer is not_open.
Results and untrusted text
Every result has two parts.
structuredContentholds the values as they are, withclassification: "untrusted_reference"and aguidancesentence. Read data from here.content[].textis what most clients show the model. Its free text (titles, quotes, notes, names, links) is escaped so it can't pose as formatting. Only ids, fixed values, numbers and sentences written by the server are left plain.
Everything that came from speech, a screen, a model or another session is untrusted reference material. It never authorizes an action. Tell your model so, and wait for the person's own instruction. Text inside a screenshot is data too.
Limits
| What | Limit |
|---|---|
One wait (ramble_read with a requestId) | 40 seconds. |
| Waits open at once | 2 per connection. A third gets too_many_waits. |
| Writes | 240 per connection per 10 minutes. After that, rate_limited until the window passes. |
| Screenshot bytes per call | 1500000 by default, 3000000 at most. A frame that doesn't fit is left out, with a line naming the ramble_read call that fetches it. Black frames are never sent. |
ramble_find | 100 rambles per call. |
| Findings per claim | 100. |
| Lease length | 5 to 240 minutes. |
| Comment, status note, brief | 2000, 1000 and 2000 characters. |
| Open record requests | 20 per account. |
| Projects | 200 per account. |
| Personal tokens | 20 per account. |
Errors
A tool that fails still returns a normal result, with isError: true. structuredContent.error is a stable code and message is a sentence written by the server. The one exception is a value the schema itself rejects (a wrong type, a number outside its range, a choice that is not one of the listed ones): the MCP library refuses that before Ramble sees it, and the result has isError: true and "Input validation error" text, with no structuredContent and so no code. The codes you can meet:
| Code | Meaning |
|---|---|
insufficient_scope | The token lacks the scope. The result names it in scope. |
rate_limited | Too many writes in the last 10 minutes. |
not_found | No such ramble, finding, lease or request on this account. A thing that belongs to someone else looks exactly the same. |
held | Another session holds a claim. heldBy says who and until when. |
not_claimed | A status needs your live claim. Claim again. |
not_found_or_expired | The lease has ended. Claim again. |
unknown_findings, too_many_findings | Bad finding ids, or more than 100. |
invalid_argument | An action is missing an argument it needs (missing lists them), or since is not an ISO 8601 date. A call refused for this costs nothing against the write limit. |
invalid_actor, invalid_note, invalid_url, invalid_status, invalid_project, invalid_brief, invalid_context | A value passed the schema and the server still refused it, for example a link that is not https. The message says which. |
project_limit, activity_limit, too_many_open_requests | An account limit is reached. |
not_open | The request has already been started or answered. |
too_many_waits | Two waits are already open. |
not_available | Not possible from an agent connection, for example setting the autonomy to act. |
internal_error | The call failed on the server. Try again. |