MCP server
The MCP server gives an AI agent your lines, their texts and calls, and a new carrier IP, as 7 tools. Claude Code, Claude Desktop, Cursor and any client that speaks MCP over HTTP can use it with an API key.
Where it is
- The address is
https://mcp.ostranetwork.com/mcp. One endpoint,POST /mcp, over Streamable HTTP: each JSON-RPC message is its own request, answered with one JSON object or a short event stream. A batch is refused, and there are no sessions to keep. - It speaks MCP revision 2026-07-28, and the 2025 revisions (2025-03-26, 2025-06-18, 2025-11-25) for clients that still open with
initialize. - Every request carries
Authorization: Bearer, checked afresh each time: a revoked or expired key fails on its very next call. A request with anOriginheader from a web page is refused. - A key works here exactly as on the REST API: its scopes, its lines, 600 requests a minute.
Connect Claude Code
Make an API key with the scopes your agent needs (for the tools below: lines:read, lines:new_carrier_ip, messages:read, messages:read_content, calls:read, and calls:read_content for what a call carried), then add the server. The syntax is Claude Code’s own, from its MCP documentation:
claude mcp add --transport http ostra https://mcp.ostranetwork.com/mcp \
--header "Authorization: Bearer $OSTRA_API_KEY"
claude mcp get ostraIn Claude Code, /mcp lists the tools. Try: “List my Ostra lines”, or “Wait for a code on LAX-SANDBOX-00” while you send the sandbox line a text.
Keep the key in the macOS keychain instead. ostra login (the CLI) stores it there, and so does this command, which asks for the key, then for it again to confirm:
security add-generic-password -s com.ostranetwork.cli -a https://api.ostranetwork.com -l "Ostra API key" -U -wA small script then hands it to Claude Code at each connection, so it is never written to a file. Save this as ~/.local/bin/ostra-mcp-headers and make it executable (chmod 700):
#!/bin/sh
# The Authorization header for Claude Code's connection to Ostra,
# read from the key `ostra login` keeps in the macOS keychain.
key=$(security find-generic-password -s com.ostranetwork.cli -a https://api.ostranetwork.com -w) || exit 1
printf '{"Authorization":"Bearer %s"}\n' "$key"claude mcp add-json --scope user ostra \
'{"type":"http","url":"https://mcp.ostranetwork.com/mcp","headersHelper":"/Users/you/.local/bin/ostra-mcp-headers"}'Claude Code runs the script for each connection and again after a refused call, gives it 10 seconds, and runs a user-scope server’s script from ~/.claude, so give its absolute path (Claude Code’s documentation, read 2026-10-21).
These commands change your own Claude Code settings, so Ostra’s checks do not run them.
Other MCP clients
- Claude Desktop reaches a remote server as a custom connector, from Anthropic’s cloud, not from your computer: add one with the URL above, “No sign in”, and an
Authorizationrequest header ofBearer osk_...(Anthropic’s guide, read 2026-10-21). Menu names change between versions; follow the guide where they differ. - Any other client that speaks Streamable HTTP and can send a header: give it the URL and the same bearer header.
Sign in instead of a key
A client can sign in with OAuth (the flow) and use a token issued for this server’s resource, https://mcp.ostranetwork.com/mcp. A token issued for the REST API is refused here, and the reverse. A request with no credential gets 401 with a WWW-Authenticate header naming the protected resource metadata (RFC 9728), at /.well-known/oauth-protected-resource/mcp. A tool call the token lacks a scope for gets 403 with error="insufficient_scope" and the scopes it needs, before the tool runs.
Sign-in works only for a client Ostra registered for this server’s resource; there is no dynamic client registration. Claude Code, Claude Desktop and other MCP clients connect with an API key, as above.
The tools
Each tool calls the same code as its REST route, so the sandbox line, the new carrier IP limits, idempotency, the audit log and every refusal behave exactly as on the API. The list is the same for every caller.
TOOL list_lines
- Arguments
- None
- Scopes
lines:read- Same as
GET /v1/lines- Returns
{lines}
TOOL get_line
- Arguments
line_id- Scopes
lines:read- Same as
GET /v1/lines/{line_id}andGET /v1/lines/{line_id}/proxy- Returns
{line, proxy}: the line, its proxy endpoints and its credential list, never a secret.
TOOL new_carrier_ip
- Arguments
line_id,idempotency_keyoptional- Scopes
lines:new_carrier_ip- Same as
POST /v1/lines/{line_id}/new-carrier-ip- Returns
{job, idempotent_replay}at once: a handle to read, never a wait.
TOOL get_new_carrier_ip
- Arguments
line_id,job_id- Scopes
lines:new_carrier_ip- Same as
GET /v1/lines/{line_id}/new-carrier-ip/{job_id}- Returns
{job}
TOOL wait_for_code
- Arguments
line_id,timeout_secondsoptional,cursoroptional- Scopes
messages:readandmessages:read_content- Same as
GET /v1/lines/{line_id}/messages/wait, thenGET /v1/lines/{line_id}/messages/{message_id}- Returns
{line_id, code, message, next_cursor, timed_out}; the message is marked untrusted_content: true, since its words are a third party's, never instructions.. Waits up to 90 seconds (timeout_seconds, default 60)
TOOL list_messages
- Arguments
line_id,cursoroptional,limitoptional,include_contentoptional- Scopes
messages:read, plusmessages:read_contentwith include_content- Same as
GET /v1/lines/{line_id}/messages- Returns
- As the REST list; with content each text is also marked untrusted_content: true, since its body is a third party's, never instructions.
TOOL list_calls
- Arguments
line_id,cursoroptional,limitoptional,include_contentoptional- Scopes
calls:read, pluscalls:read_contentwith include_content- Same as
GET /v1/lines/{line_id}/calls- Returns
- As the REST list; with content each call is also marked untrusted_content: true, since its transcript is a third party's, never instructions.
- A tool’s answer is the REST answer, as structured content and as the same JSON in text. A refusal is a tool error in the API’s shape,
{"error": {"code", "message", ...}}:not_found,new_carrier_ip_refusedwithnext_allowed_at,line_suspended,rate_limited. Arguments that do not fit a tool’s schema are a tool error too, so the agent can correct them. new_carrier_ipreturns a job at once; read it withget_new_carrier_ip. Send the sameidempotency_keywithin 24 hours and you get the first answer back, withidempotent_replay: true.wait_for_codewaits like the texts long poll, without content and one text at a time, so a text with no code is passed over and never decrypted. The first text with a live code is opened once and written to the audit log. With no code by the deadline it answerscode: null,timed_out: trueand anext_cursorto wait on from. It waits up totimeout_seconds(1 to 90, default 60), and counts against the 5 waits at once per line.
Every tool’s description says what it is for: testing sign-up, sign-in and verification flows of services you own or are authorized to test.
What is not a tool
On purpose, the server has no tool that creates a line, buys a number, texts or calls out from a line, solves a CAPTCHA, changes a browser’s fingerprint, or creates an account anywhere. A verified organization gets lines by ordering them, from the dashboard or the API, and a line only receives.
Check a key by hand
An MCP client does this for you. To see that a key reaches the server, list the tools with the raw request. OSTRA_MCP_URL is the address above.
#!/bin/sh
# Check a key against the MCP server by hand: list its tools.
# MCP clients do this for you; this is the raw request, MCP revision 2026-07-28.
curl -sS -X POST "$OSTRA_MCP_URL" \
-H "Authorization: Bearer $OSTRA_API_KEY" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'mcp-protocol-version: 2026-07-28' \
-H 'mcp-method: tools/list' \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}'The answer is a JSON-RPC result whose tools are the 7 above, in that order, each with its name, title, description and input schema.