Docs/AI agents

AI agents

Give a coding agent the one thing it cannot get anywhere else: a place to actually run the Apex it writes, and evidence that it works.

AI agents

Overview

Your agent can write Apex anywhere. Here it can also run it — instantly, locally, with no org — and get evidence back: which tests passed, what each line cost, who wrote every field on every record.

That is the whole difference. Salesforce ships an MCP server for metadata; several tools will read your source. None of them can execute the code. An agent that cannot execute is guessing, and an agent that guesses about Apex guesses about triggers, order of execution, governor limits and null semantics — the four things it is worst at.

nimbus mcp is a Model Context Protocol server over stdio. It starts one runner — database, project sources, schema — and holds it open for the session, so a tool call costs a test run, not a cold start.

What an agent gets that it does not have today:

  • Execution. Run the suite, one class, one method, or a block of anonymous Apex. Sub-second on a warm runner.
  • Evidence. Not a log to parse — structured failure diagnostics with the assertion's operands, the SOQL/DML tail before the break, and the source around it.
  • A recording. When a test fails, Nimbus re-runs it with tracing on. The agent gets a span tree of what actually executed and what each step cost.
  • Causality. Every field write of that run, with the layer that made it — Apex, trigger, flow, roll-up, formula, platform default — and the exact frame. Why is this value what it is stops being a guess.
  • Safety. Everything is rolled back unless the agent explicitly asks to commit.

Nothing here reaches a Salesforce org. There is no sandbox to refresh, no deploy to wait for, and no shared org for the agent to break.

Setup

The server discovers your project from its working directory, so point cwd at the folder containing sfdx-project.json.

Claude Code — one command, run from the project root:

bash
claude mcp add nimbus -- nimbus mcp

Cursor.cursor/mcp.json in the project root:

json
{
  "mcpServers": {
    "nimbus": {
      "command": "nimbus",
      "args": ["mcp"]
    }
  }
}

Any other MCP client — the same shape, with an explicit working directory when the client does not launch the server from your project:

json
{
  "mcpServers": {
    "nimbus": {
      "command": "nimbus",
      "args": ["mcp", "--parallel", "4"],
      "cwd": "/absolute/path/to/your/salesforce/project"
    }
  }
}
FlagDefaultDescription
--parallel0Worker count for run_apex_tests (0 = NumCPU). Fixed at server start; the runner sizes its connection pool and worker fan-out from it, so it cannot change per call.
--coveragetrueCollect coverage so get_coverage works after every run. Set false in tight agent loops that never read coverage.
--read-onlyfalseRefuse any call that could leave a durable change behind. Tests and Apex still run — they roll back. The safe default for a shared machine or a CI job; see Safety caps.
--verbosefalseRaise the stderr log to debug. Safe: stdout is reserved for the protocol.

First call. Have the agent call nimbus_capabilities. It returns the tool catalog, the versioned payload schemas, the licence tier, and what this particular server can answer right now — cheaper than discovering the surface by trying tools and reading errors.

Stdio safety. Stdout carries JSON-RPC framing and nothing else. The runner writes to io.Discard, and the process-global stdout is rerouted to stderr for the server's lifetime so a stray print anywhere in the call tree cannot corrupt the stream. Tail stderr to watch the agent work: every tool call logs a structured start/complete line.

Tool catalog

Grouped by what an agent is trying to do. Every payload is bounded — a truncated list always reports the true count alongside — and every payload states what it does not establish, because an agent that reads a filtered result as the whole picture is the failure mode worth engineering against. For the authoritative list at any version, have the agent call nimbus_capabilities.

Run things

MethodDescription
run_apex_testsRun tests by pattern ("*", a class, Class.method, or a path). Returns totals and per-failure detail — message, file, line, structured diagnostics — plus a replayable recording of the failures. Bound with max_failures; disable the recording with record_trace=false.
execute_anonymousExecute a block of anonymous Apex. Rolled back by default; commit=true keeps the writes. Returns success, captured System.debug output, and the exception on failure.
run_methodInvoke one Apex method with typed JSON arguments — the tool renders the Apex literals, so no argument is a quoting exercise. Returns a typed result. Rolled back by default.
run_mutation_testsPro. Mutate the code and report which mutants survive — the holes in your assertions, not just the uncovered lines.

Understand a failure

MethodDescription
get_test_failureEverything about one failure in one call: message, stack, structured diagnostics, the source lines around it, and the replay pointer. Answers from the last run when it covered that test, otherwise runs just that test.
explain_failureThe same failure through the shared nimbus.explain/v1 contract, identical to `nimbus explain --json`, so a human and an agent never read divergent accounts. redact=true masks runtime values for output that will be pasted elsewhere.
triage_failuresGroup a run’s failures by the cause the engine recorded when each happened — not by matching message text. Use it on a wall of red to find how many distinct problems there actually are. Failures the engine could not classify come back individually rather than folded into a plausible-looking group.
get_execution_traceA recorded run’s spans: the call tree with timing, parent links, per-span governor cost, and SOQL/DML/trigger/flow summaries. Frozen schema, see below.
query_field_historyPro. Every field write of a recorded run, with what it replaced, the layer that wrote it, and the exact frame.
get_governor_usagePer-test governor consumption from the last run, plus the peak per limit. A limit that scales with record count is the signature of un-bulkified code.

Read the project

MethodDescription
queryRun a read-only SOQL SELECT against the local database. DML and non-SELECT statements are rejected — use execute_anonymous for writes.
describe_schemaAn SObject’s fields (type, required, unique, references, relationship name, picklist values) and child relationships. Omit the argument to list every SObject the project has schema for.
get_coverageLine coverage from the last run. Narrow it to one class to also get the uncovered executable lines in its file and per-method figures — a method with calls=0 was never executed. Uncovered lines are omitted from a whole-project report, where they would be thousands of numbers nobody can act on.
list_test_classesEvery @isTest class with its source file. Runs nothing.
query_graphWhat a change to one class can reach: dependents, dependencies, and the tests that actually executed it. Read the returned limits — this is reachability for navigation, not a set of affected tests, and a trigger dispatching through custom metadata is invisible to it.

Write metadata

MethodDescription
scaffold_metadataGenerate valid-by-construction source: Apex classes and triggers, LWC, Aura, Visualforce, objects, fields, labels, custom metadata, permission sets, field sets, record types, validation rules, list views. Call it with no arguments for the catalog of kinds and the options each accepts; dry_run=true previews without writing.

Discover

MethodDescription
nimbus_capabilitiesThe catalog above, plus the versioned schemas and their compatibility promise, the licence tier, and this server’s current state — coverage on or off, which runs have been recorded, what the last run did.

Why scaffold rather than write the XML. The metadata file vocabulary is not the describe vocabulary: a file wants Html, EncryptedText, MultiselectPicklist where a describe answers TEXTAREA, ENCRYPTEDSTRING, MULTIPICKLIST. An agent writing from memory reaches for the describe spelling, the field is created, it deploys, and it behaves wrongly. Every kind scaffold_metadata emits has a round-trip test proving Nimbus can read back what it wrote.

Resources

A tool call is a verb an agent spends a turn on. A resource is a noun it can attach: a stable URI, an unchanging shape, no arguments to get wrong, and cacheable by the client — so re-reading your project's schema costs nothing.

Everything under nimbus:// is read-only and executes nothing: no test run, no Apex, no DML, no files written. That is what makes them safe to fetch speculatively, and it is why neither a safety cap nor read-only mode ever refuses one. An agent that has spent its entire budget can still read every resource below.

MethodDescription
nimbus://capabilitiesThe same payload nimbus_capabilities returns — the tool and resource catalog, the frozen schemas, the licence tier, the active caps and what has been spent against them. Built by the same code as the tool, so the two can never disagree.
nimbus://projectPackage directories from sfdx-project.json, class and trigger counts, and the @isTest class inventory with source paths.
nimbus://parityThe published compatibility scorecard shipped with this binary: per-repository pass rates over the open-source corpus and how many failures were verified against a real org. Note what it is not — a measurement of your project.
nimbus://coverageLine coverage from the most recent run, overall and per class.
nimbus://coverage/{class}One class, including the executable lines no test reached — the list you need to write the missing test.
nimbus://schemaEvery SObject this project has schema for.
nimbus://schema/{object}One SObject’s fields (type, required, unique, references, picklist values) and child relationships.

The two templated URIs are RFC 6570 resource templates, and each one's list endpoint enumerates its members — so an agent never has to guess a valid substitution. Every payload is bounded and publishes the true count beside a possibly-truncated list.

An empty answer always says why. Read nimbus://coverage before anything has run and you get available: false with a reason, not a zero-percent report. An empty payload that reads like a real measurement is the one wrong answer an agent acts on with total confidence.

No subscriptions. Nimbus does not advertise resources/subscribe. Coverage does change after a run, so a subscription would be genuinely useful — but the transport cannot service one, and advertising a capability that answers method-not-found is worse than not having it. Each payload states the staleness instead: coverage is the last run's, and re-reading after a run is how you get the new one.

The agent loop

The tools are shaped for one sequence, and it is worth putting in your agent's instructions:

text
run_apex_tests { "pattern": "AccountServiceTest" }
  -> 2 failed; each failure carries trace_run_id

get_test_failure { "test": "AccountServiceTest.testInsert" }
  -> the assertion, its operands, the SOQL/DML tail, the source lines

  ... edit the class ...

run_apex_tests { "pattern": "AccountServiceTest.testInsert" }
  -> green

When the failure is not obvious from the assertion, the evidence chain goes one level deeper:

text
get_execution_trace { "run_id": "<from the failure>",
                      "test": "AccountServiceTest.testInsert",
                      "kinds": ["soql", "dml", "trigger"] }
  -> what actually ran, in order, and what each step cost

query_field_history { "run_id": "<the same>", "field": "Account.Rating" }
  -> Rating was "" -> "Hot", written by AccountTrigger.beforeInsert:7

On an unfamiliar codebase where the first run returns hundreds of failures, start with triage_failures instead. Grouping a wall of messages is exactly the task a model does confidently and wrongly; the engine already recorded the cause of each one.

Start narrow. Every payload reports the true count of what it capped, so an agent can always widen deliberately. The defaults are chosen so a first call is cheap.

Writes and rollback

An agent is allowed to execute arbitrary Apex here because by default it can observe anything and change nothing.

execute_anonymous and run_method run inside an isolating transaction that is discarded the moment the call finishes. Every response carries a committed field saying which happened, so the agent never has to infer it. Passing commit: true is a separate, explicit act, and the response's limits then say the writes are still there.

Two boundaries worth knowing, both stated in the payloads themselves:

  • The isolating transaction defers constraint checks, so an insert that would violate a unique or foreign-key constraint on commit reports success under rollback.
  • Seed data is committed deliberately, before the transaction opens. Rollback never reverts seeding.

Test runs are unaffected either way: each test already runs in its own transaction and is rolled back after.

scaffold_metadata is the one tool that writes to your source tree. It refuses to overwrite an existing file unless asked, refuses to write outside the project root, and dry_run: true reports what it would create without creating it.

Safety caps

An MCP session hands an autonomous agent a warm runner and no natural stopping point. A loop that re-runs the suite after every edit, or retries a failing block with a slightly different guess, costs the agent nothing it can feel — so nothing stops it. These are the ceilings that make an unattended session bounded.

All of them are off by default. An interactive developer should never meet a limit they did not set. Turn them on where a session is unattended or shared.

FlagDefaultDescription
nimbus.mcp.max-test-runs-per-session0Cap on test-executing calls: run_apex_tests, triage_failures, get_test_failure, explain_failure, run_mutation_tests. 0 disables.
nimbus.mcp.max-apex-executions0Cap on agent-supplied Apex: execute_anonymous and run_method. Metered separately — it is the surface where the agent writes the code. 0 disables.
nimbus.mcp.max-wall-clock0Total session wall time after which metered calls are refused, e.g. 30m. 0 disables.
nimbus.mcp.read-onlyfalseRefuse any call that could leave a durable change behind. Also available as `nimbus mcp --read-only`.

A refusal always names itself. Exceeding a cap is a tool error stating which cap, what it was set to, how much was used, and the exact property that set it. Nothing stalls, and nothing quietly returns less than it was asked for — an agent that cannot see what refused it retries against a wall it does not know is there.

Reads are never metered. The caps apply only to calls that run something. Every read tool and every nimbus:// resource keeps working after a budget is spent, so an agent can always collect the results the session already produced.

Read-only mode

nimbus mcp --read-only is the safe default for a shared machine or a CI job. What it refuses is narrower than it sounds, and deliberately so:

  • execute_anonymous and run_method with commit: true — the same call without commit still runs, and you still get the result, the debug output and the exception. Only the writes are discarded.
  • scaffold_metadata unless dry_run: true, which returns the exact files it would have written.

Tests still run. Every test already executes in its own transaction and rolls back, so a test run leaves nothing behind — refusing them would make read-only mode useless on a test runner while adding no safety. What the mode gates on is whether a change can outlive the call, not whether the call touches the database.

The flag and the property compose one way only: either turns the mode on, and neither turns it off. A safety switch a config file can silently disarm is not a safety switch.

Both the caps and the mode are startup-bound and reported in nimbus_capabilities under state.budget, with used, limit and remaining, so an agent can pace itself rather than discover a ceiling by hitting it.

Execution traces

A replayable trace is not a cheap trace. Stepping through an execution needs per-line spans, which only exist at verbose tracing, and verbose costs roughly 65% more interpreter time and tens of megabytes per run — measured, not estimated. Recording every run to keep the few that matter is a bad trade.

So Nimbus retraces instead. When a run fails, the failing tests are executed a second time with tracing on. It pays only when something failed, and only for what failed: on a 1,212-test suite with 24 failures, that is about 2% of the work rather than all of it.

run_apex_tests does this automatically and returns the recording's id on every failure it covers. Pass record_trace: false to skip it.

Three things a reader of a trace must not get wrong, all of which the payload states outright:

  • A retrace is a second execution, not a replay of the first. A test that passes the second time is order-dependent or flaky. The manifest records reproduced: false and the trace's limits say the recording shows a passing execution — it is never presented as showing the failure.
  • A test that declares its failures deliberate is not recorded. A fixture that is supposed to fail is not a failure anyone needs to replay.
  • An absent governor number means "not measurable", not zero. Counters reset inside a span — Test.startTest does exactly that — make the delta meaningless, and claiming a number would be a lie.

Span schema v1

get_execution_trace returns nimbus.span/v1, stamped with schemaVersion: 1. It is an API, not an output format: integrations persist and diff these payloads, so the shape is fixed.

  • Additive only within version 1. Fields are added; none is removed, renamed, retyped or given a new meaning.
  • A breaking change ships as version 2 alongside 1, never in place. A consumer pinned to 1 keeps working.
  • Consumers must ignore unknown fields, and must treat an unknown span kind as opaque rather than an error — new kinds appear within v1 as the engine grows.
  • An absent number means "not measurable", never zero.

The document:

json
{
  "schema": "nimbus.span/v1",
  "schemaVersion": 1,
  "run": {
    "run_id": "2026-08-26T10-11-12_ab12cd34",
    "kind": "failure",
    "spans_recorded": 812,
    "spans_returned": 200,
    "truncated": true,
    "steppable": true,
    "failure": { "tests": [ { "class": "...", "method": "...", "reproduced": true } ] }
  },
  "filters": { "max_spans": 200, "include_events": true, "include_attributes": false },
  "spans": [
    {
      "span_id": "a1b2c3d4e5f60718",
      "parent_span_id": "0918273645abcdef",
      "kind": "dml",
      "name": "apex.dml.insert",
      "start_ms": 12.5,
      "duration_ms": 43.0,
      "depth": 3,
      "status": "ok",
      "test": "AccountServiceTest.testInsert",
      "line": 42,
      "governor": { "dml": 1, "dml_rows": 200 },
      "dml": { "operation": "insert", "sobject": "Account", "records": 200 },
      "events": [ { "name": "apex.field.write", "at": "...", "attributes": { } } ]
    }
  ],
  "limits": [ "spans are what the recorded run captured at its trace level, ..." ]
}

Span kinds. session, test, setup, method, soql, dml, trigger, flow, validation, rollup, async, assign, branch, loop, eval, db, http, parse, lex, ast, interpreter, other. The raw span name is always carried verbatim in name; kind is a grouping over it.

Read the details. status: "unset" is OpenTelemetry's default and means no status was reported — not that the span succeeded. depth and test are computed by walking parent links, so a span whose parent is missing from a truncated file reports depth 0. Filtering by kinds can leave parent_span_id pointing at a span that is not in the document.

The full reference, including every kind-specific detail object and every event name, is committed alongside the code as docs/mcp-span-schema-v1.md, with a machine-readable JSON Schema at internal/mcp/schema/span-v1.schema.json. The emitted payload is validated against that schema in CI, so the document and the code cannot drift apart.

Field history Pro

query_field_history answers the question that costs Salesforce developers the most time and that no static tool can answer at all: why is this value what it is?

Every SObject field write a recorded run performed, in order, with what it replaced, which layer wrote it — apex, trigger, flow, rollup, formula, default, platform — and the exact frame: Class.Method:line, TriggerName.Operation:line, a flow's name, a roll-up definition, or the save-pipeline phase.

json
{
  "schema": "nimbus.field-history/v1",
  "run": { "run_id": "2026-08-26T10-11-12_ab12cd34", "ledger": "derived" },
  "count": 3,
  "total": 3,
  "writes": [
    { "test": "AccountServiceTest.testInsert", "seq": 1,
      "sobject": "Account", "field": "Rating",
      "identity": "rec-1", "record_id": "",
      "old": "", "new": "Hot",
      "writer": "trigger", "location": "AccountTrigger.beforeInsert:7" }
  ],
  "limits": [ "writes are only those the recorded run performed — ..." ]
}

Filter by sobject, field (either "Rating" or "Account.Rating"), record (an Id or a ledger identity), or test.

Identity, not Id. A record's writes before the save pipeline assigned it an Id carry an empty record_id. Follow identity to see the whole chain across that boundary.

The queryable index is derived from the recording on demand when it is missing, so a run recorded before Pro was active is still readable. The raw writes live in the trace either way — only the queryable surface is Pro.

Agent skills

MCP exposes the primitives. Skills are short playbooks that tell an agent when and how to reach for them — which tool answers which question, and in what order. Install them with nimbus skills install; the binary fetches from nimbus-skills and writes to the right path for your agent.

  • fix-failing-apex-test — the inner loop above, written out.
  • bootstrap-nimbus — set Nimbus up on a fresh SFDX project, CI snippet included.
  • apex-coverage-uplift — raise coverage by targeting the uncovered branches get_coverage reports, not by adding call-only tests.

Discovery. Nimbus publishes an ARD catalog so compatible registries and agents can find this server from task intent. ARD handles discovery only: after selecting Nimbus, the client still installs the local binary and runs nimbus mcp over stdio.