Connect your coding agent
Saram’s MCP server is at:
https://api.saram.sh/mcpThere are two ways in, and both reach the same tools:
- Sign in. The agent opens your browser, you sign in to Saram, and the tools act as you, in your organisation. Nothing is pasted anywhere and no key is stored in a file. Use this on your own machine.
- An API key. For CI and for agents with no browser. The tools act for the key’s organisation.
No tool accepts or returns a credential. The tool list is public and a tool call is not: a client connects and lists the tools before you sign in, and the first call asks for the sign-in. The last section says which block on this page was run, when, and with which version of each client.
Before you start
Section titled “Before you start”-
For signing in: a Saram account. Sign up at dashboard.saram.sh. Signing up creates your organisation; you see its runs and no others.
-
For a key: the key in your shell. Mint one on the dashboard under Settings > API keys (
/settings/keys); it is shown once. Put it in your environment, never in a file you commit:Terminal window export API_KEY=<your key>In PowerShell,
$env:API_KEY = "<your key>".
A client with no credential gets the tool list, each tool with its description and schema, the same as the MCP tools page:
curl -si -X POST https://api.saram.sh/mcp -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"annotations":{"readOnlyHint":true},"description":"List this deployment's runs, newest first, …","name":"list_runs",…},…]}}A tool call with no credential is told where to sign in, which is all a client needs to find the sign-in by itself:
curl -si -X POST https://api.saram.sh/mcp -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"list_runs","arguments":{"limit":1}}}'HTTP/1.1 401 Unauthorizedwww-authenticate: Bearer resource_metadata="https://api.saram.sh/.well-known/oauth-protected-resource/mcp"
{"detail":"Unauthorized"}The metadata names Saram’s sign-in (WorkOS AuthKit), which offers dynamic client registration and PKCE. Claude Code, Codex and Cursor use both without being told, so no client id goes in any config below.
Sign in
Section titled “Sign in”Claude Code
Section titled “Claude Code”claude mcp add --scope user --transport http saram https://api.saram.sh/mcpclaude mcp listAdded HTTP MCP server saram with URL: https://api.saram.sh/mcp to user configChecking MCP server health…
saram: https://api.saram.sh/mcp (HTTP) - ✔ ConnectedConnected means Claude Code reached the server and listed its tools; you have not signed in yet.
Start claude and ask for a tool, such as the prompt under
Check the connection. The first call answers:
MCP server "saram" needs you to sign in again (run /mcp to re-authenticate)Run /mcp, choose saram and authenticate. Your browser opens Saram’s sign-in; sign in, and the
tools act as you. The config holds the URL and no credential.
codex mcp add saram --url https://api.saram.sh/mcpAdded global MCP server 'saram'.Detected OAuth support. Starting OAuth flow…Authorize `saram` by opening this URL in your browser:https://…/oauth2/authorize?response_type=code&…&resource=https%3A%2F%2Fapi.saram.sh%2Fmcp
Successfully logged in.codex mcp listName Url Bearer Token Env Var Status Authsaram https://api.saram.sh/mcp - enabled OAuthCodex starts the sign-in as soon as the server is added and opens the link in your browser. If
the browser is already signed in to Saram, it finishes by itself. Until it finishes, codex mcp list shows Auth as Not logged in. codex mcp login saram signs in again and codex mcp logout saram signs out. ~/.codex/config.toml holds only the URL:
[mcp_servers.saram]url = "https://api.saram.sh/mcp"Cursor
Section titled “Cursor”Cursor has no command for this: the server is a file, .cursor/mcp.json in a project or
~/.cursor/mcp.json for every project.
{ "mcpServers": { "saram": { "url": "https://api.saram.sh/mcp" } }}Then finish the sign-in from Cursor’s MCP settings. Not run for this page: the file is in the shape Cursor documents, and no Cursor session was driven with it.
With an API key
Section titled “With an API key”Claude Code
Section titled “Claude Code”The simplest form is one command:
claude mcp add --scope user --transport http saram https://api.saram.sh/mcp \ --header "Authorization: Bearer $API_KEY"Added HTTP MCP server saram with URL: https://api.saram.sh/mcp to user configHeaders: { "Authorization": "[REDACTED]"}claude mcp list then reports saram as Connected. Your shell expands the variable, so the key
itself is written, in plain text, into ~/.claude.json.
To keep the key out of every file, put the server in the project’s .mcp.json and name the
variable instead. Claude Code expands ${API_KEY} when it starts, so this file can be
committed:
{ "mcpServers": { "saram": { "type": "http", "url": "https://api.saram.sh/mcp", "headers": { "Authorization": "Bearer ${API_KEY}" } } }}Claude Code asks you to approve a project’s servers the first time it starts there (claude mcp list says Pending approval until you do). With the variable unset, it warns
Missing environment variables: API_KEY.
codex mcp add saram --url https://api.saram.sh/mcp \ --bearer-token-env-var API_KEYcodex mcp listAdded global MCP server 'saram'.
Name Url Bearer Token Env Var Status Authsaram https://api.saram.sh/mcp API_KEY enabled Bearer tokenWith a variable named, Codex does not start a sign-in, and ~/.codex/config.toml holds the
variable’s name, not the key:
[mcp_servers.saram]url = "https://api.saram.sh/mcp"bearer_token_env_var = "API_KEY"Cursor
Section titled “Cursor”{ "mcpServers": { "saram": { "url": "https://api.saram.sh/mcp", "headers": { "Authorization": "Bearer ${env:API_KEY}" } } }}Cursor reads ${env:NAME} inside headers, so the file names the variable and never holds the
key. Not run for this page, as for signing in.
Check the connection
Section titled “Check the connection”Ask the agent:
Call the saram MCP server’s
list_runstool with limit 1 and print the run_id and status.
It answers your organisation’s newest run, or an empty list in a new organisation. Either way the tools are connected: a credential problem is an error, not an empty list (When it does not connect).
Testing localhost and previews
Section titled “Testing localhost and previews”Saram’s browsers run in our cloud, not on your machine. To test localhost, open a tunnel. A quick tunnel’s URL is public while it is open, so close it when the run ends. A protected preview works through a bypass secret you upload, and Saram sends it only to your preview’s own site.
A spec that names your machine is refused before anything starts:
target.entry_url names 'localhost', a cluster or local name: a run may not point at a private or cluster address unless Saram has marked the tenant trusted for itLocalhost, through a quick tunnel
Section titled “Localhost, through a quick tunnel”A Cloudflare quick tunnel needs no account:
# 1. install oncewinget install Cloudflare.cloudflared # Windowsbrew install cloudflared # macOS# 2. open the tunnel while your dev server runs; keep this terminal open for the whole runcloudflared tunnel --url http://localhost:3000# -> prints https://<four-random-words>.trycloudflare.com# 3. the spec names that URL (entry_url, and the reset hook if you have one); validate, then startYour quick Tunnel has been created! Visit it at (it may take some time to be reachable):https://<four-random-words>.trycloudflare.comOpen the URL yourself before you validate: it can take a few seconds to answer. Use it wherever
Your first run says https://staging.example.com. The tunnel’s name resolves to
Cloudflare’s public addresses, so the spec passes the rule that refuses localhost. Each start
prints a new URL, so a restarted tunnel needs the spec changed too.
Anyone who has the URL reaches your dev server while cloudflared runs: the random name is all that keeps it unknown. Stop cloudflared (Ctrl+C) when the run ends.
A dev server needs one line to work behind the tunnel.
Next.js 16, next dev. The page renders but never hydrates: Next refuses its HMR websocket
from the tunnel’s origin and logs Blocked cross-origin request to Next.js dev resource /_next/hmr. Client components then do nothing, and a persona reports the dead controls as
defects. Add this to next.config and restart next dev, or run next build && next start
instead:
allowedDevOrigins: ['*.trycloudflare.com'],Vite 8, dev. Every request answers 403, Blocked request. This host ("….trycloudflare.com") is not allowed. Add this to vite.config:
server: { allowedHosts: ['.trycloudflare.com'] },cloudflared’s --http-host-header localhost:<port> is not a fix for both: it fixes Vite, but Next
still does not hydrate, and Next’s absolute redirects then send the browser to
https://localhost:<port>/….
The run depends on your machine for its whole length:
- Keep it awake and connected. When cloudflared stops, because the laptop sleeps or the
terminal closes, the URL answers
530with a page titledCloudflare Tunnel error, and with your dev server stopped it answers502. A session that meets either page sees it instead of your product, and Saram does not yet tell that page from your product’s own. - One session at a time. Every step and every reset-hook call crosses your machine. Keep
budget.max_concurrent_sessionsat 1 unless your app keeps sessions apart by the?saram_sid=on each session’s entry URL. - Cloudflare’s limits. A quick tunnel is for testing and development, with no uptime
guarantee: 200 requests in flight at most, then
429, and no Server-Sent Events.
A protected preview, through a door
Section titled “A protected preview, through a door”A preview behind a login of its host’s, such as Vercel’s or Netlify’s, opens to a door: an
account credential you upload, carrying the headers, basic or cookies that get past it,
named in the spec’s target.auth.secret_ref. Saram’s browsers apply it only to hosts on the same
site as your entry_url, and Saram sends its basic and headers on the reset-hook call. No MCP
tool takes a credential and the dashboard’s credential form does not take a door yet, so upload it
with an API key and
create_credential. The answer’s secret_ref is the ref for the
spec. A door is removed with delete_credential.
A door of headers or basic is applied to the page’s requests, not to websocket handshakes or
service-worker fetches; those get past the host’s login only with a cookie.
Vercel
Section titled “Vercel”Vercel’s Protection Bypass for Automation, on every plan, gets past Vercel Authentication,
Password Protection and Trusted IPs. Create a secret for it in the project’s Deployment Protection
settings, put it in VERCEL_BYPASS, and upload it as a door (jq builds the body, so the secret
stays out of your shell history). The second header asks Vercel to set a cookie, which carries the
bypass to the websockets and service workers the header does not reach:
jq -n --arg s "$VERCEL_BYPASS" '{ name: "vercel-bypass", kind: "account", value: ({headers: { "x-vercel-protection-bypass": $s, "x-vercel-set-bypass-cookie": "true"}} | tojson)}' | curl -s -X POST https://api.saram.sh/api/v1/credentials \ -H 'content-type: application/json' \ -H "Authorization: Bearer $API_KEY" -d @-{ "target": { "entry_url": "https://my-app-git-my-branch-my-team.vercel.app", "auth": { "kind": "header", "secret_ref": "<the secret_ref it answered>" } }}vercel.app is a public suffix, so the preview’s site is its own host and the secret goes to that
host alone.
Netlify
Section titled “Netlify”Of Netlify’s protections, only Basic-Auth in _headers (Pro and Enterprise) has a way through
for Saram, because it is plain HTTP basic auth. Site password protection is a form with no way
around it for automation, and team login has none either. Use Basic-Auth for deploy previews, or
make the preview public:
/* Basic-Auth: saram:<password>The door is the same login as basic, uploaded as above with this value and named with
"kind": "basic":
{ "basic": { "username": "saram", "password": "<password>" } }Anything else in front of a preview, such as a Cloudflare Access service token or an
X-Preview-Token header, is a door of the same shape: its headers, its basic or its cookies.
Your first run
Section titled “Your first run”The server’s instructions tell the agent the order (validate, start, wait, read), so the prompts can be short. Use a URL of your product that Saram’s browsers can reach over the internet: a staging deployment, a preview, or a tunnel to your machine (Testing localhost and previews).
1. Validate. Free, instant, and starts nothing:
Write the smallest run spec for https://staging.example.com: one persona from
list_personas, one mission “create an account” that succeeds when the persona reaches a signed-in page, and 30 steps per session. Callvalidate_specand show me the planned sessions, the warnings and the cost ceiling. Do not start a run.
validate_spec runs every check start_run runs. On the smallest spec the tool’s schema gives:
{ "target": { "entry_url": "https://example.com" }, "personas": ["dana_first_timer@1"], "missions": [ { "id": "sign_up", "goal": "create an account", "success_criteria": [ { "kind": "judge_assertion", "value": "the persona reached a signed-in page" } ] } ], "budget": { "steps_per_session": 30 }}it answered warnings: [], one planned session (dana_first_timer@1 on sign_up) and a
cost_ceiling whose basis read:
up to $0.60: 1 session x 30 persona steps at $0.02 a step on the pilot plan, which bills steps; judging and reproduction are included.The figure is your plan’s, and it is a ceiling, not a forecast. On the Free and Pro plans a spec
with no providers block is refused: every role makes its model calls on your own provider. Only an
organisation on an older plan, or with an agreement with Saram for its models, runs such a spec on
Saram’s own credential, which cost_ceiling.model_calls says. To set up your own provider, ask the
agent to call get_provider_status, and see Providers and your own keys.
This spec’s one criterion is a judge_assertion, which is text the judge reads, not a check. Only a
url_matches criterion is checked by machine, so a session on this spec that the persona finishes
always ends with end_reason claimed_finished_unverified: the persona says it is signed in and
nothing confirmed it. For a verified ending, success_criteria_met, name in the prompt a part of
the URL your product shows once someone is signed in, such as a mission that “succeeds when the URL
contains /account”, so the agent can write a url_matches criterion
(Success criteria: what is checked).
2. Start, then wait.
Start it, wait until it is done, and tell me what it is doing between waits.
start_run answers a run_id and nothing else. wait_for_run waits up to 60 seconds a call, so
a run of several minutes takes several calls; done: false means the wait ran out, not that the
run failed. A finished run answers its status, session_counts, cost_usd and each session’s
result.
3. Read what it found.
Show me the issues that reproduced, the repro steps and a screenshot of the first, then the personas’ verdicts.
| Tool | What it returns |
|---|---|
get_issues |
One row per defect, most severe first; reproducible_only: true keeps the ones that reproduced |
get_issue |
One issue in full: persona-free repro steps, and whether it may file a ticket alone |
get_screenshot |
One screenshot as an image, by a ref from an issue’s evidence (2 MiB at most) |
get_evidence_url |
The URL, type and size of a video or anything else too large to read inline |
get_verdicts |
Each persona’s own verdict, with the diary quotes it rests on: advisory |
get_opportunities |
Persona feedback that is not a defect: advisory |
Every tool’s arguments and fields are on the MCP tools page.
Rules for your agent
Section titled “Rules for your agent”Put these in your agent’s instructions too. The server says the same in its instructions and its tools’ descriptions:
- File a ticket from an issue only when
may_file_ticket_aloneistrue. That issue reproduced without the persona layer. - When it is
false, report the issue with its evidence, marked unconfirmed, never as a confirmed bug. - Verdicts and opportunities are never tickets. They are the personas’ opinions: say whose they are, and never state them as fact.
- An issue’s
descriptionis objective; itspersona_rationaleis the persona’s voice. Quote the second; do not merge it into the first.
When it does not connect
Section titled “When it does not connect”- The tools are listed but a call answers
needs you to sign in again (run /mcp to re-authenticate), orcodex mcp listsaysNot logged in. No sign-in has finished: the list needs none, a call does. Sign in:/mcpin Claude Code,codex mcp login saram, or the sign-in in Cursor’s MCP settings.Connectedinclaude mcp listdoes not mean signed in. Server rejected the configured Authorization header (HTTP 401). The key is wrong, revoked or unexpanded; every cause is the same 401, and a wrong key is refused for the tool list too. Check thatAPI_KEYis set where the agent starts, or mint a new key.Protected resource https://api.saram.sh/mcp does not match expected .... The client was added under another address. Remove the server (claude mcp remove saram -s user,codex mcp remove saram, or the entry in Cursor’s file) and add it again as above.- A 403 after signing in. You have no seat in an organisation, or a role that may only read. Ask an owner of your organisation.
In Claude Code, an Authorization header turns signing in off (OAuth fallback is disabled when headers.Authorization is set), so a 401 there is the key’s. To sign in instead, remove the server
and add it again without --header.
What was measured
Section titled “What was measured”Every block above was run as shown except where this table says otherwise. Nothing on this page is a screenshot; the outputs are copied from a terminal, with run ids and sign-in parameters left out.
| Block | Measured |
|---|---|
| The public tool list, the 401 on a call, and its metadata | 2026-10-21, against platform-api 0.8.5 |
| Claude Code, sign in: adding the server and the first tool call | 2026-10-21, Claude Code 2.1.280, against platform-api 0.8.5 |
Claude Code, sign in: /mcp and the browser |
2026-09-19, by a person; list_runs then answered their organisation |
Codex, sign in: adding the server, the browser and list_runs |
2026-10-21, Codex CLI 0.153.4; adding it and Not logged in again against platform-api 0.8.5 |
| Cursor, both ways | Not run |
| Claude Code, key: the one command | 2026-10-21, Claude Code 2.1.280; Connected with a valid key on 2026-09-24 |
Claude Code, key: .mcp.json |
2026-09-24, five tools called with it; the approval and the missing-variable warning on 2026-10-21 |
| Codex, key: adding the server | 2026-10-21, Codex CLI 0.153.4; list_runs answered with a valid key on 2026-09-24 |
| Check the connection | 2026-10-21 (Codex, signed in) |
| Localhost: the quick tunnel | 2026-10-22, cloudflared 2026.10.3 on Windows against a static page on localhost:3000: the URL printed 4 s after the start, then 200 through it, 0.66 s for the first request and 0.14 to 0.17 s after; the same version on winget. No run started |
Localhost: the 530 and the 502 |
2026-10-22: 502 for the first seconds after cloudflared stopped, then 530 Cloudflare Tunnel error from 8 s on and still four minutes later; 502 with cloudflared up and the page’s server stopped; a second start printed a new URL |
| Localhost: the refusal, and a tunnel URL passing the rule | 2026-10-22, the control plane’s own checks run outside Saram’s cloud; validate_spec on api.saram.sh not run |
Localhost: the Next.js and Vite lines, --http-host-header |
2026-10-22, Next.js 16.3.4 next dev and Vite 8.3.5 behind a cloudflared 2026.10.3 tunnel; not re-run for this page |
| Localhost: Cloudflare’s limits | Cloudflare’s quick tunnel page, read 2026-10-22; not measured |
| Previews: Vercel | Not run against a protected Vercel preview. The upload body and the spec checked against platform-api 0.8.6’s schemas on 2026-10-22; the door itself is tested against a header-gated page, not against Vercel |
| Previews: Netlify | Not run |
Your first run, step 1: validate_spec |
2026-10-21, the spec and basis shown |
| Your first run, step 1: what the criterion gives | Not run; from the RunSpecMission schema in platform-api 0.11.0 |
Your first run, step 2: start_run |
Not run for this page; wait_for_run on a finished run, 2026-09-24 |
| Your first run, step 3: the read tools | 2026-09-24, on a finished run |
| The 401 with a wrong key | 2026-10-21, Claude Code 2.1.280; the tool list refused it too against platform-api 0.8.5 |