← All tutorials · Ohmforge

Tutorial 6 of 6

Automate with the API & MCP

Advanced 12 min

Run Ohmforge's simulator from scripts and AI agents. Real HTTP requests and responses for the operating point, transient, AC sweep and LED resistor tools, plus an MCP tools/call.

The engine that runs in the workbench is also available as four usefulweb tools, written once in TypeScript and shared between the app and the server. A netlist you simulate from a script gives the same numbers as the schematic in your browser.

ToolHTTP endpointMCP tool name
DC operating pointPOST /api/v1/tools/ohmforge-dc-operating-point/calculateohmforge_dc_operating_point
Transient simulationPOST /api/v1/tools/ohmforge-transient/calculateohmforge_transient
AC sweep (Bode plot)POST /api/v1/tools/ohmforge-ac-sweep/calculateohmforge_ac_sweep
LED resistor calculatorPOST /api/v1/tools/ohmforge-led-resistor/calculateohmforge_led_resistor

Each tool also has an HTML form page (linked from the Ohmforge landing page under For AI agents) and an entry in the OpenAPI document. The project file format is published as a JSON Schema at /project.schema.json.

All responses use the usual usefulweb envelope: summary (one sentence), result (validated against the tool's output schema), units, assumptions, warnings, sources and presentation blocks (tables, charts and CSV downloads, as on the HTML page). Values are SI base units: volts, amperes, watts, seconds, hertz.

The examples below were run against a local copy of the site. Replace $BASE with the Ohmforge host, e.g. BASE=https://ohmforge.usefulweb.site.

Every circuit as a netlist or a project

The three simulation tools accept either

  • netlist: SPICE text (the subset from tutorial 5). Its .tran and .ac lines become the defaults, or
  • project: an .ohmforge.json file as a string, exactly as File → Save writes it. The project's own analysis settings (stop time, UIC, sweep range) become the defaults.

Any parameter you pass in the request overrides those defaults.

DC operating point

curl -s $BASE/api/v1/tools/ohmforge-dc-operating-point/calculate \
  -H 'Content-Type: application/json' \
  -d '{"netlist":"* Divider\nV1 in 0 10\nR1 in out 10k\nR2 out 0 30k\n.op\n.end"}'
{
  "summary": "Divider: V(in) = 10 V, V(out) = 7.5 V; supplies deliver 2.5 mW.",
  "result": {
    "title": "Divider",
    "converged": true,
    "strategy": "Newton–Raphson",
    "iterations": 2,
    "nodes": { "in": 10, "out": 7.5 },
    "elements": [
      { "name": "V1", "type": "V", "current": -0.00025, "voltage": 10, "power": -0.0025 },
      { "name": "R1", "type": "R", "current": 0.00025, "voltage": 2.5, "power": 0.000625 },
      { "name": "R2", "type": "R", "current": 0.00025, "voltage": 7.5, "power": 0.001875 }
    ],
    "totalSupplyPower": 0.0025,
    "stress": [],
    "diagnostics": [],
    "warnings": []
  }
}

A source that delivers power has a negative power (here V1 delivers 2.5 mW). stress lists rating problems. With a 47 Ω resistor in front of a red LED (D1 b 0 LED_RED) you get:

"stress": [
  { "element": "D1", "level": "burnt",
    "message": "D1: 138.8 mA through the LED is far above its 20 mA limit – a real LED would burn out. Add or increase the series resistor." }
]

A circuit without a solution is still a 200 response. It has "converged": false and the reasons in diagnostics, e.g. "V2 closes a loop of voltage sources …" for two sources in parallel. Malformed input gets a 422 problem document:

{
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Line 4: element \"X1\" is not supported (supported: R C L V I D Q M E G K)",
  "code": "invalid_input",
  "errors": [ { "field": "netlist", "message": "Line 4: element \"X1\" is not supported (supported: R C L V I D Q M E G K)" } ]
}

Transient simulation

An RC step response, sampled every 100 µs:

{
  "netlist": "* RC step\nV1 in 0 PULSE(0 5 0 1u 1u 1m 2m)\nR1 in out 1k\nC1 out 0 100n\n.tran 10u 2m\n.end",
  "probes": ["out"],
  "tstep": 0.0001
}
{
  "summary": "2 ms transient of \"RC step\" in 138 steps; V(out) 0 V … 5 V, RMS 3.353 V.",
  "result": {
    "ok": true, "tstop": 0.002, "tstep": 0.0001, "method": "trap",
    "start": "operating point (Newton–Raphson)", "steps": 138, "rejectedSteps": 3,
    "time": [0, 0.0001, 0.0002, 0.0003, …],
    "waveforms": { "out": [0, 3.15195, 4.31926, 4.75018, …] },
    "measurements": {
      "out": { "min": 0, "max": 4.99979, "vpp": 4.99979, "mean": 2.50083, "rms": 3.35332,
               "frequency": null, "duty": null, "riseTime": 0.000226037 }
    }
  }
}

time and waveforms hold the uniform output samples. steps counts the adaptive internal steps the solver actually took. The response also includes a CSV download block with all probed nodes.

Simulate a saved project

Save the 555 blinker from the app as blinker.ohmforge.json, then wrap it into a request with jq:

jq -Rs '{project: ., probes: ["OUT", "CAP"]}' blinker.ohmforge.json \
  | curl -s $BASE/api/v1/tools/ohmforge-transient/calculate -H 'Content-Type: application/json' -d @- \
  | jq '{summary, start: .result.start, tstop: .result.tstop, OUT: .result.measurements.OUT}'
{
  "summary": "4 s transient of \"555 astable LED blinker\" in 4551 steps; V(CAP) 115.3 pV … 5.994 V, RMS 4.488 V, 987.7 mHz.",
  "start": "initial conditions (UIC)",
  "tstop": 4,
  "OUT": { "min": -3.89307e-9, "max": 7.17883, "vpp": 7.17883, "mean": 4.44817, "rms": 5.64956,
           "frequency": 0.987654, "duty": 0.533333, "riseTime": 0.000800002 }
}

The 4 s stop time and UIC came from the project's analysis settings. The net names are the ones from your net labels (OUT, CAP).

AC sweep (Bode)

{
  "netlist": "* RC low-pass\nV1 in 0 AC 1\nR1 in out 1k\nC1 out 0 100n\n.end",
  "output": "out", "input": "V1",
  "fstart": 10, "fstop": 1000000, "pointsPerDecade": 20
}
{
  "summary": "V(out): -0.0001715 dB at 10 Hz, peak -0.0001715 dB, −3 dB at 1.591 kHz.",
  "result": {
    "output": "out", "input": "V1",
    "frequencies": [10, 11.2202, 12.5893, …],
    "magnitudeDb": [-0.000171458, -0.000215849, -0.000271734, …],
    "phaseDeg": [ … ],
    "peakDb": -0.000171458, "peakFrequency": 10, "lowGainDb": -0.000171458,
    "lower3dB": null, "upper3dB": 1591.26, "unityGainFrequency": null,
    "diagnostics": []
  }
}

input names the source that gets AC = 1. inputNode (optional) reports V(out)/V(ref) instead. For a band-pass filter both lower3dB and upper3dB are set, and for an amplifier unityGainFrequency is set too. The real op-amp amplifier from tutorial 3 returns upper3dB: 271106 and unityGainFrequency: 2973980.

LED resistor calculator

{ "supply": 12, "colour": "white", "current": 15, "count": 3, "series": "E24" }
{
  "summary": "Use 160 Ω (E24, ≥ 0.125 W): 3× white LED on 12 V draws 15.57 mA (exact value 160 Ω).",
  "result": {
    "vf": 3.2, "exactOhms": 160, "standardOhms": 160, "series": "E24",
    "simulatedCurrent": 0.0155744, "simulatedLedVoltage": 9.50809,
    "resistorPower": 0.0388101, "ledPower": 0.148083,
    "recommendedRating": 0.125, "efficiency": 0.792341,
    "netlist": "* 3x white LED from 12 V\nV1 vcc 0 12\nR1 vcc a1 160\nD1 a1 a2 LED_WHITE\n…"
  }
}

The calculator works out the exact value, rounds it up to the E-series, and then simulates the result with the LED model. That's why the "verified" current (15.57 mA) differs slightly from the 15 mA target: an LED's forward voltage depends on its current. The returned netlist can be pasted straight into the workbench.

MCP

The same tools are available to AI agents through the usefulweb MCP endpoint at /mcp (Streamable HTTP, JSON-RPC 2.0). tools/list includes ohmforge_ac_sweep, ohmforge_dc_operating_point, ohmforge_led_resistor and ohmforge_transient, each with its JSON input schema. A call:

curl -s $BASE/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{
  "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": { "name": "ohmforge_ac_sweep", "arguments": {
    "netlist": "* RC low-pass\nV1 in 0 AC 1\nR1 in out 1k\nC1 out 0 100n\n.end",
    "output": "out", "input": "V1", "fstart": 100, "fstop": 100000, "pointsPerDecade": 1 } } }'
{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "content": [ { "type": "text", "text": "V(out): -0.01711 dB at 100 Hz, peak -0.01711 dB, −3 dB at 1.283 kHz.\n\n{ \"result\": { … } }" } ],
    "structuredContent": {
      "summary": "V(out): -0.01711 dB at 100 Hz, peak -0.01711 dB, −3 dB at 1.283 kHz.",
      "result": {
        "frequencies": [100, 1000, 10000, 100000],
        "magnitudeDb": [-0.0171115, -1.44507, -16.0722, -35.9647],
        "phaseDeg": [-3.59527, -32.1419, -80.9569, -89.0882],
        "upper3dB": 1282.86, …
      }
    },
    "isError": false
  }
}

One point per decade is deliberately coarse here. The −3 dB corner is interpolated between 1 kHz (−1.45 dB) and 10 kHz (−16.07 dB), which gives 1.283 kHz instead of the true 1.592 kHz. With 10 points per decade the same call returns 1.597 kHz, and with 20 it returns 1.591 kHz. Ask your agent for 20 or more points per decade whenever the corner frequency matters.

Invalid input comes back as an MCP tool error ("isError": true) whose text is the same message as the HTTP 422 detail. That lets an agent correct its netlist and try again.

Ideas

  • Regression tests for a design: keep the project in git and assert measurements.OUT.frequency with the transient tool in CI.
  • Component sweeps: loop over E12 values of a resistor in a script, call the operating-point tool for each, and plot the results.
  • Agent workflows: let an assistant draft a filter as a netlist, verify the corner frequency with ohmforge_ac_sweep, and hand you the netlist to import into the workbench (File → Import SPICE netlist…).

The engine is deterministic: the same request always returns the same numbers (only computedAt changes). Rate limits are the usual usefulweb API limits.