CLI, MCP and API
Start and follow Sinatra tasks from your terminal, from Claude Code, Cursor, claude.ai or ChatGPT, or from your own scripts.
Sinatra's task API lets you hand it work without a Linear ticket or GitHub issue. Start a task from your terminal with the sinatra CLI, from an MCP client such as Claude Code, Cursor, claude.ai or ChatGPT, or from a script over HTTP. You read the agent's answers and questions, reply, and stop a turn the same way.
All three use the same API. The CLI, Claude Code, Cursor, claude.ai and ChatGPT sign in with your Sinatra account in the browser. The HTTP API uses a personal access token, and so can the CLI on a computer without a browser. A task started this way runs like any other: it gets a sandbox, can open a pull request, and shows up in the dashboard.
Create a token
- In the dashboard, open CLI & API in the sidebar. This opens the Access tokens page at app.sinatra.dev/tokens.
- Name the token, for example
Laptop CLI, and pick when it expires: 30, 90 or 365 days. - Copy the token. It starts with
sntr_and is shown once. Afterwards the page shows only its name and last four characters.
The same page shows how to set up the CLI, Claude Code, Cursor, claude.ai, ChatGPT and other MCP clients, and the HTTP API with curl.
A token acts as you in every workspace you belong to, with your role in each. Sinatra checks your membership on every request, so if you leave a workspace, the token stops working there. Revoke a token on the same page at any time; anything using it stops working immediately. sinatra logout and DELETE /v1/me/token revoke the token they use.
Keep tokens out of committed files. Sinatra stores only a hash of each token, so a lost token can't be recovered: revoke it and create a new one.
CLI
The CLI needs Node 22 or newer. Signing in through the browser, --token and --no-browser need version 0.2.0 or newer; on an older version sinatra login asks for a token. Run npm i -g sinatra.dev to update.
npm i -g sinatra.dev
sinatra --version
sinatra loginsinatra login opens your browser on a Sinatra page that asks you to approve the Sinatra CLI. Sign in to Sinatra there if you aren't already, click Approve, and the CLI is logged in, with no token to create or copy. It then asks you to pick a default workspace if you belong to more than one. If you don't approve within 5 minutes, the CLI stops waiting.
The CLI saves the sign-in to ~/.config/sinatra/config.json ($XDG_CONFIG_HOME/sinatra if set, %APPDATA%\sinatra on Windows), readable only by you. It renews its access as it goes, and stays signed in for up to 90 days, or until it goes 30 days unused. It's listed as Sinatra CLI under Connected apps on the Access tokens page, where Disconnect signs it out at once. After either, the CLI asks you to run sinatra login again.
If the browser doesn't open, sinatra login --no-browser prints the URL to open instead. The browser has to run on the same computer as the CLI, because Sinatra sends it back to a port the CLI listens on there. On a computer you reach over SSH, or with no browser, log in with a token: sinatra login --token asks for it without echoing it.
To log in from a script, pipe the token in and pass -w <id> to set the default workspace: sinatra login -w <workspace-id> < token-file. Piped input reads a token unless you also pass --no-browser. A workspace that isn't one of yours is refused, and nothing is saved. Piped without -w, login saves no default when you belong to more than one workspace, and says how to set one.
sinatra logout ends the sign-in on Sinatra, or revokes the token you logged in with, then deletes that file. If Sinatra can't be reached, the file is still deleted and the CLI warns that the sign-in or token may still work. Disconnect or revoke it on the dashboard's CLI & API page.
Examples
sinatra "fix the flaky retry test" # start a task and follow it
sinatra "explain this" -f notes.md -f err.log # attach text files
sinatra reply <task-id> "also update the docs" # follow up, then follow the task
sinatra reply <task-id> "see this" -f err.log # attach files to a reply
sinatra show <task-id> # print the task and its conversation
sinatra tasks # the 20 most recently active tasks
sinatra tasks --status pr_open -n 50 # filter by status, show more
sinatra stop <task-id> # stop a running turn
sinatra whoami # who you're logged in as, and your workspaces
sinatra workspace # list your workspaces and the default
sinatra workspace <id> # make another workspace the default
sinatra logout # sign out and forget the login
sinatra "fix it" --no-wait --json # start a task, print it as JSONAfter you start a task or reply, the CLI prints what Sinatra posts until the turn is done, then tells you how to reply. If the task is queued because your workspace is running as many tasks at a time as its plan allows, the CLI says so. Ctrl-C stops waiting; the task keeps going. After 30 minutes, or the minutes you set with --timeout, the CLI stops waiting and exits with code 3. Pick up later with sinatra show <task-id>.
If you reply to a task that had completed, failed or been cancelled and Sinatra hasn't picked up your message within two minutes, the CLI says so and exits with code 3, without reporting the previous turn's result. A task paused on access or billing exits with code 2.
A prompt that starts with a command word still starts a task, unless it has exactly that command's shape. sinatra stop retrying on 429 starts a task, but a command word followed by a single word, such as sinatra stop now, is treated as a mistyped command. sinatra start "..." always starts one.
A prompt that starts with - goes after --, so it isn't read as an option: sinatra -- "-v is broken". A title that starts with - takes the = form: --title="-x title".
sinatra show prints the conversation only for tasks started from the CLI, MCP or API. A task started elsewhere, such as from Linear or GitHub, has its conversation there.
Flags
start here means starting a task, with or without the word: sinatra "..." and sinatra start "..." are the same.
| Flag | Commands | What it does |
|---|---|---|
-r, --repo <owner/name> | start | The repository to work on. Must be connected to the workspace. |
-f, --file <path> | start, reply | Attach a text file to the message. Repeatable. |
-t, --title <title> | start | The task's title. Defaults to the prompt's first line. |
-w, --workspace <id> | start, reply, show, tasks, stop, login | The workspace. Defaults to the one picked at login or with sinatra workspace <id>. With login, the default workspace to save. |
--timeout <minutes> | start, reply | How long to follow the task before the CLI stops waiting. Defaults to 30. |
--no-wait | start, reply | Start the task or send the reply, then return without following it. |
--json | whoami, tasks, show; start and reply with --no-wait | Print JSON and nothing else, for scripts. |
--status <status> | tasks | Only tasks in this status, such as implementing or pr_open. |
-n, --limit <n> | tasks | How many tasks to list, 1 to 100. Defaults to 20. |
--no-browser | login | Print the sign-in URL instead of opening the browser. The browser must still be on this computer. |
--token | login | Paste a personal access token instead of signing in through the browser. |
--api-url <url> | login | The API to use. Defaults to https://api.sinatra.dev. Must be https. |
-v, --version | none | Print the CLI's version. |
A flag the command doesn't use is an error, not silently ignored.
Repository. Inside a git checkout, the task runs on the checkout's GitHub repository. The CLI looks at the origin remote, then upstream. Another GitHub remote counts only when neither of those is on GitHub. SSH host aliases from ~/.ssh/config, such as git@github-work:owner/repo.git, count when they point at github.com. If origin's repository isn't connected to the workspace, the CLI says so and tries upstream's, so a fork falls through to the repository it forks. When none is connected, Sinatra's agent picks one. The CLI prints which remote it used. A repository you name with -r is never replaced: if it isn't connected, the start fails. Outside a GitHub checkout, without -r, the agent picks.
Attachments. Files go into the new task's first message or into your reply, marked as file content rather than instructions. Only text files can be attached, up to 80 KB together. Anything larger is refused, not cut short.
Retries. Every request that changes something carries an idempotency key. The CLI tries a request up to three times on a dropped connection or a server error, and a retry never starts a second task or sends a message twice.
Exit codes
| Code | Meaning |
|---|---|
0 | The turn finished, or the command succeeded. |
1 | An error: bad arguments, not logged in, an invalid token, a refused request, or a network failure. |
2 | The task failed, was cancelled, or is paused on access or billing. |
3 | Still working when the CLI stopped waiting, or Sinatra hasn't picked up your reply yet. |
MCP server
Sinatra is a remote MCP server at https://api.sinatra.dev/v1/mcp. It uses the Streamable HTTP transport. Claude Code, Cursor, claude.ai and ChatGPT sign in with your Sinatra account over OAuth, like the CLI, so there's no token to create or paste. Clients that take a URL and headers can use a personal access token in an Authorization: Bearer header instead.
When an app signs in, Sinatra opens a page in your browser that names the app and asks you to approve it. A connected app acts as you in every workspace you belong to, with your role in each, like a token does. It stays signed in for up to 90 days, or until it goes 30 days without connecting, then asks you to sign in again. Approved apps are listed under Connected apps on the Access tokens page. Disconnect ends an app's access right away: its next call is refused, and it has to sign in again to reconnect.
Claude Code
claude mcp add --transport http --scope user sinatra https://api.sinatra.dev/v1/mcpThen run /mcp in Claude Code, or claude mcp login sinatra, and sign in to Sinatra in your browser. --scope user makes Sinatra available in every project.
To use a token instead:
read -rs SINATRA_TOKEN && claude mcp add --transport http --scope user sinatra \
https://api.sinatra.dev/v1/mcp --header "Authorization: Bearer $SINATRA_TOKEN"When the command waits, paste your token and press Enter; nothing shows as you paste. read -rs keeps the token out of your shell history.
Cursor
Open the Add to Cursor install link, or add this to ~/.cursor/mcp.json, or to .cursor/mcp.json in a project:
{
"mcpServers": {
"sinatra": {
"url": "https://api.sinatra.dev/v1/mcp"
}
}
}The Cursor app then asks you to sign in to Sinatra. In the Cursor CLI, run cursor-agent mcp enable sinatra, then cursor-agent mcp login sinatra.
claude.ai and Claude Desktop
- In claude.ai, open Customize, then Connectors, click Add and choose Add custom connector.
- Name it
Sinatraand enterhttps://api.sinatra.dev/v1/mcpas the URL. - Click Connect, then approve the app on Sinatra's page.
Claude Desktop uses the same connectors as claude.ai, so once you've added Sinatra in one, it's in the other.
To revoke access, click Disconnect under Connected apps on Sinatra's Access tokens page; removing the connector in claude.ai alone doesn't.
ChatGPT
ChatGPT adds MCP servers as plugins. Starting a task needs a custom MCP server with write actions, which OpenAI lists for ChatGPT Business, Enterprise and Edu workspaces, on the web. On Business, an admin or owner adds it.
- In ChatGPT, open the Plugins page (chatgpt.com/plugins), click + and choose Add custom MCP server.
- Name it
Sinatra, enterhttps://api.sinatra.dev/v1/mcpas the server URL, and choose OAuth for authentication. ChatGPT finds Sinatra's sign-in settings itself; keep the registration method it picks. - Tick I understand and want to continue, click Create as a plugin, then Continue to Sinatra.
- Approve the app on Sinatra's page, which names it as ChatGPT, described by chatgpt.com.
To use it, mention @Sinatra in a chat, or click Try in chat.
To revoke access, click Disconnect under Connected apps on Sinatra's Access tokens page; deleting the plugin in ChatGPT alone doesn't.
Other clients
We've tested sign-in with Claude Code, Cursor, claude.ai and ChatGPT only. Another client that takes a server URL and headers can use a token:
{
"mcpServers": {
"sinatra": {
"url": "https://api.sinatra.dev/v1/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}Tools
Every tool except whoami takes a workspace_id. Call whoami first to get yours.
| Tool | Arguments | What it does |
|---|---|---|
whoami | none | Who the token acts as, and the workspaces you belong to, with your role in each. |
list_tasks | workspace_id, status, limit (1–100), cursor | The workspace's tasks, most recently active first, with status and pull request. |
get_task | workspace_id, task_id | One task's status, title, repository and pull request. |
start_task | workspace_id, prompt, title, repo, idempotency_key | Starts a task. Omit repo to let the agent choose. |
follow_up | workspace_id, task_id, text, idempotency_key | Sends a message on a task started from the CLI, MCP or API. |
get_messages | workspace_id, task_id, after, limit (1–200) | The task's conversation, oldest first. Pass after to read only what's new. |
stop_task | workspace_id, task_id | Stops a running task's current turn. Any member of the workspace can stop a task. |
Each tool also carries a display title and hints about what it does: whoami, list_tasks, get_task and get_messages only read; start_task and follow_up start work that can push to GitHub; stop_task interrupts work in progress. Clients such as Claude can use the hints to decide which calls need your approval.
Tools return the same JSON as the HTTP API, as text and as structured content. A refused call, including an input-validation failure, returns a tool execution error (isError: true) that says what to fix.
The server answers each request with plain JSON; it opens no event stream and keeps no session. Modern clients use protocol version 2026-07-28 without an initialization handshake: include params._meta["io.modelcontextprotocol/protocolVersion"] on each request, a matching MCP-Protocol-Version header, and Mcp-Method matching the JSON-RPC method. For tools/call, also send Mcp-Name matching params.name (the =?base64?…?= encoding is supported). server/discover returns supported versions, capabilities and server information. Modern results include resultType: "complete" and server information in _meta; discovery and tool lists have a one-hour, private cache lifetime. Unsupported metadata versions receive HTTP 400 / -32022 with all supported versions; missing or mismatched headers receive HTTP 400 / -32020. Modern initialize, ping and other unknown methods receive HTTP 404 / -32601.
Modern requests must also include params._meta["io.modelcontextprotocol/clientCapabilities"] (use {} when no client capabilities are needed). Omitting it returns HTTP 400 / -32602 before tool execution.
Requests without the protocol-version metadata keep the legacy behavior: initialize supports 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05, negotiating an unknown version to 2025-11-25. Send the negotiated version in the MCP-Protocol-Version header on subsequent legacy requests. Mcp-Session-Id and Last-Event-ID are ignored; GET and DELETE return HTTP 405 in both eras.
If an MCP request includes an Origin header, it must be a well-formed HTTPS origin or an HTTP loopback origin (127.0.0.1, localhost or [::1], with an optional port). Other values, including null, receive HTTP 403 with a JSON-RPC error and no id, before authentication. CLI and server-side clients can omit Origin. Authentication remains bearer-only; browser cookies do not authenticate MCP requests.
HTTP API
The base URL is https://api.sinatra.dev. Send your token on every request:
Authorization: Bearer sntr_...Request and response bodies are JSON. The examples below set SINATRA_TOKEN to your token and WS to a workspace id.
Who am I
GET /v1/me returns the token's user and the workspaces they belong to. Use it to find workspace ids.
curl -s https://api.sinatra.dev/v1/me -H "Authorization: Bearer $SINATRA_TOKEN"{
"user": { "id": "cmabc...", "githubLogin": "octocat", "name": "Mona" },
"workspaces": [{ "id": "cmxyz...", "name": "acme", "role": "member" }]
}Revoke the token
DELETE /v1/me/token revokes the token that sends it and returns 204 with no body. Requests with that token return 401 invalid_token afterwards. sinatra logout calls it when you logged in with a token.
curl -s -X DELETE https://api.sinatra.dev/v1/me/token -H "Authorization: Bearer $SINATRA_TOKEN"The task object
Task endpoints return tasks in this shape:
{
"id": "api3f9c...",
"status": "implementing",
"source": "api",
"title": "Fix the flaky retry test",
"repo": "acme/web",
"pullRequest": { "url": "https://github.com/acme/web/pull/42", "merged": false },
"queued": false,
"createdAt": "2026-10-13T14:02:11.000Z",
"lastActivityAt": "2026-10-13T14:09:47.000Z"
}source is api for tasks started from the CLI, MCP or API, linear for Linear issues, and github_issue for GitHub issues. Other sources can appear, so treat an unknown value as a task started elsewhere. title can be null for a task whose issue has no title. repo is null until a repository is chosen, and pullRequest is null until one opens. queued is true while a turn waits because your workspace is running as many tasks at a time as its plan allows, which the dashboard shows as Queued. See What the states mean for status values.
List tasks
GET /v1/workspaces/:id/tasks lists the workspace's tasks, most recently active first.
| Query | Default | Description |
|---|---|---|
status | Only tasks in this status, such as implementing or pr_open. | |
limit | 50 | 1 to 100. |
cursor | nextCursor from the previous page. |
curl -s "https://api.sinatra.dev/v1/workspaces/$WS/tasks?status=pr_open&limit=20" \
-H "Authorization: Bearer $SINATRA_TOKEN"{ "tasks": [{ "id": "api3f9c...", "status": "pr_open", "...": "..." }], "nextCursor": "..." }To get the next page, pass nextCursor back as cursor. Treat it as opaque. nextCursor is null on the last page.
Get a task
GET /v1/workspaces/:id/tasks/:taskId returns one task. It works for any task in the workspace, wherever it started.
Start a task
POST /v1/workspaces/:id/tasks starts a task and returns 202 with the task and the id of your first message.
| Field | Required | Description |
|---|---|---|
prompt | Yes | What to do, as you'd ask a teammate. Up to 100,000 characters. |
title | No | Up to 200 characters. Defaults to the prompt's first line, up to 80 characters. |
repo | No | owner/name of a repository connected to the workspace. Omit it to let the agent choose. |
curl -s -X POST "https://api.sinatra.dev/v1/workspaces/$WS/tasks" \
-H "Authorization: Bearer $SINATRA_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fix-retry-test-1" \
-d '{"prompt": "Fix the flaky retry test in packages/api", "repo": "acme/web"}'{ "task": { "id": "api3f9c...", "status": "pending", "source": "api", "...": "..." }, "messageId": "fix-retry-test-1" }Send a message
POST /v1/workspaces/:id/tasks/:taskId/messages sends a follow-up and returns 202 with the message's id. Send {"text": "..."}, up to 100,000 characters.
Any member of the workspace can follow up on a task started from the API, CLI or MCP. Follow up on other tasks, such as Linear or GitHub issues, where they started; here they return 409 not_an_api_task. A task that has been idle for a while picks up again when you follow up.
curl -s -X POST "https://api.sinatra.dev/v1/workspaces/$WS/tasks/$TASK/messages" \
-H "Authorization: Bearer $SINATRA_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-2" \
-d '{"text": "Also update the docs"}'{ "messageId": "reply-2" }Read the conversation
GET /v1/workspaces/:id/tasks/:taskId/messages returns the conversation of a task started from the API, CLI or MCP, oldest first.
| Query | Default | Description |
|---|---|---|
after | The id of the last message you've read. Returns only later messages. | |
limit | 100 | 1 to 200. |
{
"messages": [
{ "id": "cmm1...", "messageId": "fix-retry-test-1", "role": "user", "kind": "message", "body": "Fix the flaky retry test in packages/api", "createdAt": "2026-10-13T14:02:11.000Z" },
{ "id": "api:7d2e...", "messageId": null, "role": "agent", "kind": "clarify", "body": "Should the retry keep its 3-attempt limit?", "createdAt": "2026-10-13T14:06:30.000Z" }
],
"next": null
}role | kind | What it is |
|---|---|---|
user | message | A message a person sent. messageId is its idempotency key, or a generated id. |
agent | response | The agent's answer for a turn. |
agent | clarify | A question the agent needs answered to continue. |
platform | response or error | A message from Sinatra itself, such as a refusal, an error, a stop, or files the agent produced. |
When next isn't null, more messages follow: pass next as after to get the next page. An after that isn't a message of this task returns 400 invalid_cursor.
Stop a task
POST /v1/workspaces/:id/tasks/:taskId/stop stops the task's running turn and returns 202 with {"accepted": true}. It works on any task in the workspace, and any member can stop one. A completed or cancelled task, or one whose session has already ended, returns 409 not_running.
Idempotency
Send an Idempotency-Key header on POST .../tasks and POST .../messages so a retry is safe. The key is 1 to 128 printable ASCII characters, with no spaces.
- A start retried with the same key returns the same task and never starts a second one. The same key with a different prompt, repository or title returns
409 idempotency_conflict. - A message retried with the same key is sent once. Keys belong to their sender: two people using the same key send two messages.
- The key becomes the message's
messageId.
If Sinatra saves your request but can't hand it to the agent yet, it returns 503 unavailable. On a start, the body includes the new taskId and messageId: retry with the same key, or send a message on that task, which sends your prompt first. On a message, retry with the returned messageId as the Idempotency-Key so it's sent once.
Errors
Errors return a JSON body such as {"error": "invalid_prompt"}.
| Status | error | Meaning |
|---|---|---|
400 | invalid_prompt | prompt or text is missing, empty, or over 100,000 characters. |
400 | invalid_title | title is empty or over 200 characters. |
400 | invalid_repo | The repository isn't connected to this workspace. |
400 | invalid_idempotency_key | The Idempotency-Key isn't 1 to 128 printable characters. |
400 | invalid_status | No task status has that name. |
400 | invalid_limit | limit is out of range. |
400 | invalid_cursor | cursor is malformed, or after isn't a message of this task. |
401 | invalid_token | The token is missing, malformed, expired or revoked. |
404 | not_found | No such task, or you aren't a member of the workspace. |
409 | not_running | The task isn't running. |
409 | not_an_api_task | The task started elsewhere, such as Linear or GitHub; it has no conversation here. |
409 | idempotency_conflict | The key was already used with different content. |
429 | too_many_running | You already have 5 CLI, MCP or API tasks starting, running or queued in this workspace. The body carries the cap as a number: "limit": 5. |
429 | bad_request | Over the request rate limit. Wait for the Retry-After header. |
503 | unavailable | Sinatra couldn't reach the task. Retry with the same key. |
A workspace you don't belong to answers 404, never 403, so the API doesn't reveal which workspaces exist.
Limits
- Running tasks: up to 5 tasks you started from the CLI, MCP or API, per workspace, starting, running or queued at once. Tasks started elsewhere, such as from Linear or GitHub, don't count, and neither does a task waiting for your reply. Messages on existing tasks aren't capped.
- Request rate: per token and per endpoint, 600
GETrequests a minute and 120POSTorDELETErequests a minute. Over that, the API returns429with aRetry-Afterheader. Every MCP call goes to one endpoint, so all MCP calls, including pollingget_messages, share one budget of 120 a minute. - Message size: 100,000 characters per prompt or message.
- Tokens: expire after at most 365 days.
How replies work
The conversation holds final answers and questions, not progress. The agent posts one reply per turn: its answer, or a question when it needs your input. Sinatra adds its own notices, such as errors and refusals. Thoughts and progress updates aren't posted.
The conversation is also on the task's page in the dashboard, where any member of the workspace can read it and reply.
There is no streaming. To wait for a reply, poll GET .../messages with after set to the last message id you read, and check the task's status with GET .../tasks/:taskId. The turn is done when the task stops working, for example when it reaches replied or pr_open. The CLI polls every three seconds.
A message sent while the agent is working doesn't change the current turn. It waits and runs as the next turn. While a task is queued, its conversation stays quiet. Its queued field says so, and the dashboard shows it as Queued.
Tasks started this way count against your workspace's plan like any other task. A running turn takes one of the plan's tasks at a time; past that it's queued, and if the queue is full, Sinatra says so in the conversation instead of starting it. See How the queue works. It also needs model access, like a task you delegate from Linear or GitHub. See Models.