Using the API from an AI coding agent

The MCP server

https://thrustlab.com/mcp is a remote Model Context Protocol server over Streamable HTTP. It is the shortest path: the assistant calls tools instead of writing HTTP.

No account is needed. Every tool works anonymously. If you have a ThrustLab API key, send it as a static bearer token and the server identifies you — there is no OAuth flow to complete.

Use the URL exactly as written, with no trailing slash. https://thrustlab.com/mcp/ reaches the same server. Both forms respond directly without a redirect. The registry manifest and every example below use the URL without the slash, so paste that form.

The MCP server
ToolWhat it doesRead-only
search_componentsFind motors, propellers and batteries, with their datasheet valuesYes
get_componentOne database row by id, with its sourceYes
estimate_propellerThrust, torque and power for one propeller from its printed sizeYes
simulate_powertrainSolve a whole powertrain at one operating point; returns a share URLNo
sweep_powertrainThe same across a throttle or airspeed range, up to 25 pointsNo
get_resultRead a stored result by share token, or by simulation id with a keyYes
get_validation_summaryMeasured accuracy for a model, by partition and channelYes

Every tool carries readOnlyHint, destructiveHint and openWorldHint, so a client can decide for itself what needs confirmation. Resources: thrustlab://llms-full and one per validation report. One prompt: Size a powertrain.

Claude Code

claude mcp add --transport http thrustlab https://thrustlab.com/mcp

# With an API key:
claude mcp add --transport http thrustlab https://thrustlab.com/mcp \
  --header "Authorization: Bearer $THRUSTLAB_API_KEY"

Or in .mcp.json / ~/.claude.json. The type field is required — an entry with a url and no type is read as a stdio server and skipped.

{
  "mcpServers": {
    "thrustlab": {
      "type": "http",
      "url": "https://thrustlab.com/mcp"
    }
  }
}

Cursor

.cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project.

{
  "mcpServers": {
    "thrustlab": {
      "url": "https://thrustlab.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_THRUSTLAB_API_KEY" }
    }
  }
}

Drop the headers object to connect anonymously.

Codex CLI

~/.codex/config.toml, or .codex/config.toml for one project.

[mcp_servers.thrustlab]
url = "https://thrustlab.com/mcp"

# Optional: send an API key read from the environment.
bearer_token_env_var = "THRUSTLAB_API_KEY"

ChatGPT developer mode

  1. In ChatGPT, open Settings → Security and login and turn on Developer mode.
  2. Go to chatgpt.com/plugins and select the plus button.
  3. Enter a name and description.
  4. Under Connection, enter https://thrustlab.com/mcp, including the /mcp path and no trailing slash.
  5. Create the connection and review the discovered tools.

Then add the connection from the tools menu in a new conversation. Developer mode availability depends on your account and workspace policy.

OpenAI Responses API

from openai import OpenAI

client = OpenAI()
response = client.responses.create(
    model="gpt-5.1",
    tools=[
        {
            "type": "mcp",
            "server_label": "thrustlab",
            "server_description": "Electric UAV powertrain simulation and a public component database.",
            "server_url": "https://thrustlab.com/mcp",
            "require_approval": "never",
        }
    ],
    input="What propeller and motor should I use for a 1.2 kg quadcopter on 4S?",
)
print(response.output_text)

Add "authorization": "YOUR_THRUSTLAB_API_KEY" to the tool object to call as your account.

What to ask for

Component data is published under CC BY 4.0. When an assistant quotes a value, the citation line is Data: ThrustLab component database (thrustlab.com), CC BY 4.0. Every simulation result names the propeller model that produced it — quote that name with the number, and hand the user the share URL.

Machine-readable surfaces

Machine-readable surfaces
URLWhat it is
https://thrustlab.com/mcpThe MCP server, Streamable HTTP. Anonymous, or an API key as a bearer token.
https://thrustlab.com/openapi.jsonThe full OpenAPI document. Generate a client from it.
https://thrustlab.com/llms.txtA one-line-per-page index of the documentation.
https://thrustlab.com/llms-full.txtEvery documentation page as one plain-text file.
https://thrustlab.com/docs/guides/errors#<code>The row for one error code. Every error's doc_url is this URL.

An agent that hits an error can fetch the doc_url it was given and read that code's row without searching. The base URL is https://thrustlab.com, so every endpoint is https://thrustlab.com/v1/.... The Python SDK reads THRUSTLAB_API_KEY from the environment. THRUSTLAB_BASE_URL overrides the base URL.

Drop this into your agent's instructions

Put this block in a project's AGENTS.md or CLAUDE.md.

## ThrustLab API
Read the key from the `THRUSTLAB_API_KEY` environment variable.
Never write a key into a file, a commit, or a log line.
Use base URL `https://thrustlab.com`; every endpoint is under `/v1/`.
Send `Authorization: Bearer $THRUSTLAB_API_KEY`.
Prefer the Python SDK (`pip install thrustlab`) over hand-rolled HTTP.
Send an `Idempotency-Key` header on every POST, PATCH and DELETE.
Reuse the same key when retrying the same logical write; a different body under
the same key is rejected with 409.
Poll async runs every 2 to 5 seconds and back off when nothing changes.
Never poll in a tight loop.
On any 429, wait the number of seconds in the `Retry-After` header. Do not use a
fixed backoff: the four budgets have windows of a minute, an hour and a day.
Submission limits, per account, per minute: 30 to `POST /v1/sweeps`, 30 to
`POST /v1/dynamic-simulations`, 120 to `POST /v1/simulations`.
To evaluate many combinations, send ONE sweep with a `component_axes` axis
rather than a loop of single-point runs. A loop burns the submission limit and
recompiles the solver for every point.
`sweep.<axis>.steps` is the NUMBER OF POINTS, not a step size:
{"start": 40, "stop": 100, "steps": 4} gives 40, 60, 80, 100.
Do not scrape the component catalog. `GET /v1/components` filters and sorts
server-side (`kv[gte]`, `resistance[lte]`, `sort_by`), and `ids_only=true`
returns up to 5000 ids in one response with a `truncated` flag.
`GET /v1/components/{id}/specs` returns the same datasheet the list rows carry,
so narrow with the list first rather than fetching every row one at a time.
For a dynamic run, read `progress` on the resource to see how far it has got.
The `estimated_duration_s` from `/estimate` is simulated mission time, not wall
time.
To stop work, call `POST /v1/simulations/cancel-selected` with the run ids
rather than cancelling one at a time. Ids returned under `requested` are still
running; poll them until they turn `canceled`.
Every error carries `type`, `code`, `request_id` and `doc_url`. Branch on
`code`, log `request_id`, and fetch `doc_url` when the code is unfamiliar.
Treat ramps from near-zero duty as a risk: one external study observed
non-converged rows in their first milliseconds, enough of a short flight's rows
to trigger `solver_no_convergence`.
Start ramps at a duty the ESC runs at, or step to the target.

One sweep beats a loop of single points

A component_axes entry is {"axis": "propeller"|"motor"|"battery", "slots": [<rotor group index>, ...], "component_ids": [...]} and needs at least two ids. It crosses with the numeric axes, so four propellers and four throttles is one 16-point grid rather than sixteen requests. Each returned point names the component it was solved with in point["inputs"]["component"], a list of {axis, slot, id, name}. A sweep returns names and ids only, never a catalog component's spec values. sweep_config.component_axes[] on the sweep resource mirrors the same structure: {axis, slot, slots, components: [{id, name}]}.

"""Component axis x throttle: auth -> submit -> wait() -> inspect points.

Run it:
    export THRUSTLAB_API_KEY=key_...        # never hard-code the key
    python examples/sweeps/component_axis_grid.py

Crosses a propeller component axis with a throttle range, so one request solves
every propeller at every throttle. It takes the first four catalog propellers
from client.components.list(type="propeller") and crosses them with four
throttle settings: 16 points in one submission.

Each returned point names the propeller that produced it at
point["inputs"]["component"], a list of {axis, slot, id, name}.

Sixteen single-point submissions would be sixteen requests against the
per-minute submission limit and sixteen cold solves; this is one request and
one warm solver.

`sweep.throttle.steps` is the NUMBER OF POINTS, not a step size.
"""

from thrustlab import Client

client = Client()  # reads $THRUSTLAB_API_KEY from the environment

project = client.projects.create(name="sdk component-axis example")

# The fixed half of the configuration — same verification combo as
# examples/sweeps/run_and_poll.py. Resolve by name, or paste explicit IDs.
motor = client.components.find(name="BadAss 2826-820Kv")
battery = client.components.find(
    name="Liperior 5000mAh 4S 35C 14.8V Lipo Battery With XT90 Plug"
)

# The swept half: the first four catalog propellers. `.data` is the first page
# of the cursor pager, so this is one request. A component axis needs at least
# two IDs — a one-value axis is not a sweep, and the server rejects it with
# `invalid_component_axis`.
props = client.components.list(type="propeller", limit=4).data
prop_ids = [p["id"] for p in props]
print("propeller axis:", ", ".join(p["name"] for p in props))

sweep = client.sweeps.create(
    project_id=project["id"],
    battery_component_id=battery["id"],
    airspeed_m_s=0.0,
    density_kg_m3=1.225,
    battery_charge_pct=100,
    # `propeller_component_id` here is the BASE value for slot 0. The component
    # axis below overrides it at every point, so it only has to be valid.
    rotor_groups=[
        {
            "label": "main",
            "count": 4,
            "motor_component_id": motor["id"],
            "propeller_component_id": prop_ids[0],
            "throttle_pct": 70,
        }
    ],
    sweep={
        # One bool per rotor group: this group follows the swept throttle axis.
        "rotor_sweep_mask": [True],
        # `steps` is the NUMBER OF POINTS, not a step size: 40, 60, 80, 100.
        "throttle": {"mode": "range", "start": 40, "stop": 100, "steps": 4},
        # `slots` indexes rotor_groups. Two or more slots would make this ONE
        # shared grid dimension applied to each of them, not a cross-product.
        "component_axes": [
            {"axis": "propeller", "slots": [0], "component_ids": prop_ids},
        ],
    },
)
print(f"created {sweep['id']}: {sweep['total_points']} points in one request")

result = client.sweeps.wait(sweep["id"], timeout=900)
print(f"final status: {result['status']}")

if result["status"] == "completed":
    print("\npropeller            | throttle | thrust/rotor (N)")
    for pt in client.sweeps.list_points(sweep["id"]):
        # `inputs["component"]` names the component(s) this point was solved
        # with: one {axis, slot, id, name} entry per active component axis.
        # It carries names and IDs only — never the component's spec values.
        chosen = ", ".join(c["name"] for c in pt["inputs"]["component"])
        thrust = pt["rotors"]["1"]["thrust_n"]
        print(f"  {chosen:<18} | {pt['inputs']['throttle']:>7}% | {thrust:.2f}")

The sweep resource carries credits_breakdown: {points, rotors, cu_per_point_per_rotor, total}, so the cost of a grid is readable before and after the run rather than inferred from the balance. input_snapshot on a sweep is the request body as submitted, so an agent can read back exactly what it asked for instead of keeping its own copy.

What the errors mean

The full catalog is at Errors, one row per code, and every error's doc_url deep-links to its row.

What the errors mean
codeWhat to do
create_rate_limitedWait Retry-After seconds. Consider one sweep instead of many submissions.
rate_limit_exceededWait Retry-After seconds. The catalog is open and has no datasheet budget.
unknown_filterThe filter key is not one this component type carries. The message lists the valid keys.
solver_no_convergenceThe run was rejected. Check error.details, and start throttle ramps above the ESC's minimum duty.

See Rate limits for the budgets and Async resources for the run lifecycle.