Skip to content

Connect your coding agent

Saram’s MCP server is at:

https://api.saram.sh/mcp

There 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.

  • 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:

Terminal window
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 OK
Content-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:

Terminal window
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 Unauthorized
www-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.

Terminal window
claude mcp add --scope user --transport http saram https://api.saram.sh/mcp
claude mcp list
Added HTTP MCP server saram with URL: https://api.saram.sh/mcp to user config
Checking MCP server health…
saram: https://api.saram.sh/mcp (HTTP) - ✔ Connected

Connected 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.

Terminal window
codex mcp add saram --url https://api.saram.sh/mcp
Added 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.
Terminal window
codex mcp list
Name Url Bearer Token Env Var Status Auth
saram https://api.saram.sh/mcp - enabled OAuth

Codex 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 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.

The simplest form is one command:

Terminal window
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 config
Headers: {
"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.

Terminal window
codex mcp add saram --url https://api.saram.sh/mcp \
--bearer-token-env-var API_KEY
codex mcp list
Added global MCP server 'saram'.
Name Url Bearer Token Env Var Status Auth
saram https://api.saram.sh/mcp API_KEY enabled Bearer token

With 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"
{
"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.

Ask the agent:

Call the saram MCP server’s list_runs tool 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).

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 it

A Cloudflare quick tunnel needs no account:

Terminal window
# 1. install once
winget install Cloudflare.cloudflared # Windows
brew install cloudflared # macOS
# 2. open the tunnel while your dev server runs; keep this terminal open for the whole run
cloudflared 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 start
Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):
https://<four-random-words>.trycloudflare.com

Open 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 530 with a page titled Cloudflare Tunnel error, and with your dev server stopped it answers 502. 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_sessions at 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 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’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:

Terminal window
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.

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.

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. Call validate_spec and 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.

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_alone is true. 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 description is objective; its persona_rationale is the persona’s voice. Quote the second; do not merge it into the first.
  • The tools are listed but a call answers needs you to sign in again (run /mcp to re-authenticate), or codex mcp list says Not logged in. No sign-in has finished: the list needs none, a call does. Sign in: /mcp in Claude Code, codex mcp login saram, or the sign-in in Cursor’s MCP settings. Connected in claude mcp list does 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 that API_KEY is 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.

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