Use the MCP server#
MCP, the Model Context Protocol, lets an AI agent call a service’s tools over HTTP; Streamable HTTP is its remote transport. The hosted TauOpt MCP server exposes the same experiment surface as the hosted REST API (create, suggest, observe, best, list, archive) as twelve tools over MCP Streamable HTTP, so an agent in Claude Code or any other MCP client can drive an optimization loop in conversation. The server is https://mcp.tauopt.com/mcp, and the tau_ token you mint at app.tauopt.com/settings/tokens is the credential (Hosted beta: getting started walks the sign-in and the mint). This page was verified on the validation date by reading the server source in the closed service repository and by running it: the health check, the four 401 rows, the twelve tools as served, and one experiment driven through create, ask, report, best, and archive from the Python SDK; the 403 for a browser Origin was checked against the live server on 2026-10-20. Where the MCP server sits relative to the REST API and the engine is drawn on Run against the hosted service.
With the hosted examples over plain HTTP, this page is the closed beta’s whole surface: the package is not distributed until the public release, neither path installs the package (five of the hosted scripts need their objective’s libraries), and the token you minted rides every request.
Before you start#
An MCP client that speaks Streamable HTTP and can send a static
Authorizationheader. The server authenticates with a bearer token only; there is no OAuth flow, so a client that insists on an OAuth login cannot connect.The MCP URL,
https://mcp.tauopt.com/mcp: a separate host from the REST base URL (the service runs its MCP server beside the API), and the endpoint is the/mcppath on it.Your token:
tau_followed by exactly 43 URL-safe characters (A-Z,a-z,0-9,_,-). The server checks that shape before verifying the token.The server answers with the engine its lane runs: the first suggestion’s
provenance.engine_versionnames it, and on the validation date (2026-10-31) the beta ran engine 0.4.0, the version these pages describe (Run against the hosted service explains when the pages can lead the lane).
Add the server to Claude Code#
With
TAUOPT_TOKENexported in your shell, register the server:claude mcp add --transport http tauopt https://mcp.tauopt.com/mcp --header 'Authorization: Bearer ${TAUOPT_TOKEN}'--transport httpselects Streamable HTTP and--headerattaches the bearer token to every request. The single quotes pass${TAUOPT_TOKEN}as a literal, so the token never enters the command line (argv is world-readable on shared machines); Claude Code expands the reference from its own environment at connection time. The alternative is to register the server without--headerand add the header by hand to the server’s entry in Claude Code’s config file (~/.claude.jsonfor the default scope). The flag names are fromclaude mcp add --help. Keep the default scope (local): theprojectscope writes a.mcp.jsoninto the repository; with the literal reference that file holds no token, but a server entry for your closed-beta host does not belong in a shared checkout.Confirm the entry with
claude mcp get tauopt. It prints the header as the stored${TAUOPT_TOKEN}reference, never the token (the status line carries the CLI’s check mark or cross):tauopt: Scope: Local config (private to you in this project) Status: Connected Type: http URL: https://mcp.tauopt.com/mcp Headers: Authorization: Bearer ${TAUOPT_TOKEN}
The first probe after a quiet spell can report
Failed to connectwithVersion negotiation probe timed outwhile the service wakes; run it again. A rejected token also shows as a failed connection: the twocurllines of the 401 table below tell the cases apart. In a session the twelve tools appear under thetauoptserver name, for examplemcp__tauopt__create_experiment.
Configure another MCP client#
For clients that take a servers map, the entry is an HTTP server with one static header (Cursor’s mcp.json, Claude’s custom connectors with a request header, and the Python SDK are shown on Hosted beta: getting started):
{
"mcpServers": {
"tauopt": {
"type": "http",
"url": "https://mcp.tauopt.com/mcp",
"headers": {
"Authorization": "Bearer tau_<your-token>"
}
}
}
}
Whether a given client accepts a static header on a remote HTTP server is that client’s documentation to answer; the server needs exactly this header on every request, tools/list included.
Check the connection#
GET /health on the MCP host needs no token and answers {"status":"ok"}; it says the server is up and nothing about your token. Every request to /mcp is authenticated before it is served. The two lines that tell the cases apart:
curl -sS https://mcp.tauopt.com/health
curl -sS -i -X POST -H "Authorization: Bearer $TAUOPT_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' https://mcp.tauopt.com/mcp
The first prints {"status":"ok"}. The second answers HTTP/2 200 with an mcp-session-id header and the server’s initialize result when the token is good (both lines were run against the service on the validation date), or the 401 of the table below with the error_description that names your case.
A client that opens a session this way carries the protocol version from then on; a client that discovers the modern protocol instead (the Python SDK’s default) sends the protocol’s per-request fields on every call for you. A request you send yourself on the modern stateless protocol – a tools/list from a script, say – must carry them: in the request’s params._meta, the protocol version under io.modelcontextprotocol/protocolVersion and the client’s capabilities (an empty object is fine) under io.modelcontextprotocol/clientCapabilities; and in headers beside the body: MCP-Protocol-Version with the same version, Mcp-Method with the request’s method, and, on a tools/call, Mcp-Name with the tool’s name (the server checks that the headers and the body agree). With the headers but without the envelope the server answers 400 with a JSON-RPC error -32602 whose message names the two keys; without the headers the request takes the legacy session path and answers a plain 400 for the missing session; either way it is not served. A call that carries the envelope and the headers:
curl -sS -X POST -H "Authorization: Bearer $TAUOPT_TOKEN" -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":{}}}}' https://mcp.tauopt.com/mcp
The answer is a JSON-RPC result carrying the tool list (the twelve names below); the validation run’s, cut after the first tool’s opening:
{"jsonrpc":"2.0","id":1,"result":{"cacheScope":"private","resultType":"complete","tools":[{"annotations":{"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"readOnlyHint":false},"description":"Create an optimization experiment ...
The same request without the _meta envelope answered 400 (the status line shown with -i):
HTTP/2 400
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"params._meta must be an object carrying the required 'io.modelcontextprotocol/protocolVersion' and 'io.modelcontextprotocol/clientCapabilities' envelope keys"}}
The Python SDK sends the envelope and the headers for you; Claude Code, the custom connectors and Cursor carry the protocol’s fields themselves, so none of them needs the envelope by hand. A failed check answers 401 with a JSON body {"error": "invalid_token", "error_description": "..."} and a WWW-Authenticate: Bearer realm="tauopt" challenge (the challenge carries error="invalid_token" for the last two rows):
Request |
Status |
|
|---|---|---|
No |
401 |
|
A scheme other than |
401 |
|
A token that fails the |
401 |
|
A well-formed token that is unknown, revoked, or issued for another lane |
401 |
|
Two more answers are not verdicts on your token. 429 with {"error": "rate_limited"} and Retry-After: 10 means first-sight token verification is being rate-limited: retry after the delay. 503 with {"error": "verification_unavailable"} and Retry-After: 10 means the server could not reach its verifier. A 403 with {"error": "origin_forbidden"} appears only when a browser-style Origin header is present and not allowed; non-browser MCP clients send none. A verified token is cached at this boundary, so a token revoked or rotated on the tokens page keeps working on the MCP server for a short window: up to a minute on the server image the beta runs (the revoke answer’s mcp_revocation_lag_s reports the same figure, 60). The REST API refuses a revoked token at once.
The twelve tools#
Each tool maps to one route of the hosted REST API (the Client wraps every one of them). Experiment and suggestion ids are integers in tool arguments; they come back as strings in results, so pass them as integers in the next call (a numeric string is accepted). The arguments, the served descriptions, and the read-only, destructive, and idempotent annotations of every tool are on MCP tools.
Tool |
One line |
|---|---|
|
Create an experiment from |
|
List active experiments, newest first, with |
|
Fetch one experiment, archived included, with its progress |
|
Change the name, budget, bandwidth, metadata, or metric thresholds |
|
Soft-delete an experiment; there is no un-archive |
|
Ask for the next point; the open suggestion holds a bandwidth slot until observed or dismissed |
|
List suggestions, all or by |
|
Dismiss every open suggestion |
|
Dismiss one open suggestion |
|
Report a value, values, or a failure against a suggestion or raw assignments |
|
List observations in id order |
|
The best assignments so far |
Mind the wait budget on next_suggestion#
The server never retries next_suggestion, because a retried call would mint a second open suggestion: after a timeout or transport error, call list_suggestions with state="open" before asking again. The server allows max_wait_s plus 30 s (90 s when max_wait_s is unset) before it reports a timeout, and a failed call comes back as an isError tool result whose text is one JSON object carrying a code; the arithmetic and the shape are on MCP tools.
Drive one experiment from a conversation#
You speak; the agent calls the tools. The arguments are shown as the agent sends them.
You: “Create a TauOpt experiment named lr-sweep: lr is a double from 1e-5 to 0.1 on a log scale, layers is an integer from 1 to 8; maximize val_score; budget 30.” The agent calls
create_experiment:{ "name": "lr-sweep", "parameters": [ {"name": "lr", "type": "double", "bounds": [1e-05, 0.1], "log_scale": true}, {"name": "layers", "type": "integer", "bounds": [1, 8]} ], "metric": {"name": "val_score", "objective": "maximize"}, "observation_budget": 30 }
The result carries the experiment’s
id; the steps below use17.You: “Ask it for the next point.” The agent calls
next_suggestionwith{"experiment_id": 17}and gets back an open suggestion with its ownidand theassignmentsforlrandlayers.You train with those assignments and report: “val_score was 0.91 for that suggestion.” The agent calls
report_observation:{"experiment_id": 17, "suggestion_id": 42, "value": 0.91}A run that crashed is evidence too; report it with
{"experiment_id": 17, "suggestion_id": 42, "failed": true}instead of a value.Repeat steps 2 and 3. With the default
parallel_bandwidthof 1, asking twice without reporting answers 429bandwidth_full: report, or calldismiss_suggestionwith{"experiment_id": 17, "suggestion_id": 42}, before the next ask.You: “What is the best so far?” The agent calls
best_nowwith{"experiment_id": 17}and reads{count, data}.You: “Archive it.” The agent calls
archive_experimentwith{"experiment_id": 17}; the slot under the active-experiment cap is freed, and the experiment stays readable throughget_experimentandbest_now.
Run the stdio bridge against your own service#
This section is for operators running their own TauOpt service; the service is a separate component from this repository. The tauopt-mcp console script serves the same twelve tools over stdio, with no boundary authentication of its own, and forwards every call to the API you name:
Variable |
Required |
Meaning |
|---|---|---|
|
yes |
The token sent on every call; the bridge refuses to start without it |
|
yes |
The service’s base URL, for example |
|
no |
|
The bridge reads TAUOPT_API_URL; the Python snippets elsewhere use TAUOPT_BASE_URL, a name you choose – the client reads neither. A missing variable exits with tauopt-mcp: TAUOPT_TOKEN is required (refusing to start without a token) or tauopt-mcp: TAUOPT_API_URL is required. The bridge does no token checking itself: a bad token surfaces as an unauthorized tool error at the first call. It needs Python 3.12 or newer and is installed from the service repository (uv sync --frozen in its mcp-server directory puts tauopt-mcp in that environment’s bin). Registration in Claude Code, where stdio is the default transport:
claude mcp add tauopt -e TAUOPT_TOKEN='${TAUOPT_TOKEN}' -e TAUOPT_API_URL=http://localhost:8080 -- /path/to/mcp-server/.venv/bin/tauopt-mcp
As above, the single quotes pass the literal reference and Claude Code expands it from its own environment at connection time, so the token stays out of the command line; the hand-edit of the config file is the alternative.
Every tool, argument, and annotation is listed in MCP tools; the same loop from Python is Run against the hosted service.