MCP Server
The sfdt Model Context Protocol (MCP) server exposes Salesforce release-management, testing, and
governance tools directly to agentic workflows — Claude Code, Cursor, Copilot, or any standard MCP
client.
Start the server
sfdt mcp startThis starts a JSON-RPC 2.0 stdio stream on standard input/output. All operational logs are
routed to stderr to keep the RPC channel clean. The server is stateless per request — every
tool call shells out to the sfdt CLI.
Configuration
Configure it under the mcp key in .sfdt/config.json:
{
"mcp": {
"enabled": true,
"parking": {
"enabled": true,
"thresholdBytes": 50000,
"ttlSeconds": 86400,
"cacheScope": "session"
}
}
}Pinning which projects a server serves
Every tool accepts a projectRoot, and that value is chosen by the model — while SFDT’s AI
surfaces feed that model untrusted org content (Apex compile errors, flow metadata, deploy failure
text). So a call you read as “query the current project” can name a different checkout and run
against that org, with the tool list still presenting it as read-only.
Since 0.26.0 the default depends on how the server was launched — which is what separates the safe case from the abusable one.
| Launched | projectRoot behaviour |
|---|---|
Inside a project (an sfdx-project.json exists at or above the launch directory) | Serves that project only. projectRoot may restate the root, or name any path under it, but not point elsewhere. |
| Outside any project (neutral) | Unrestricted. Routing every call is this mode’s entire purpose. |
So a multi-project server is one you start outside any project — which is what it already was. To keep a project-bound server serving several checkouts, name them:
export SFDT_MCP_PROJECT_ROOTS="/work/customer-a:/work/customer-b"Colon-separated absolute paths. The allowlist wins wherever it is set.
Two details worth knowing:
- Being “inside a project” is decided by finding the project, not by whether its configuration
loads. A project whose
.sfdt/config.jsonorsfdx-project.jsonis malformed still binds the server — those files are committed and arrive with whatever repo was cloned, so the alternative would let a repository decide that the server reviewing it should be unrestricted. - Paths are compared physically, after resolving symlinks. A committed symlink pointing out of the project does not satisfy the check.
Upgrading from 0.25.0. If you start the server inside one project and route calls to another,
that now fails with “outside this server’s project”. Either start it outside any project, or
list the roots in SFDT_MCP_PROJECT_ROOTS. 0.25.0 shipped this as opt-in only, so the default was
unchanged and the model still chose the root.
Added in 0.25.0. Also in 0.25.0: sfdt_retrofit requires confirmExecution on both paths (without
--execute it still retrieved over your checkout and committed), and sfdt_pr_comment takes a PR
number rather than a URL — a URL names a repository too — redacts its body, and requires
confirmation instead of presenting as read-only.
Exposed tools
Pre-deployment validation
| Tool | Arguments |
|---|---|
sfdt_preflight | strict (boolean, optional) |
sfdt_validate | targetOrg (required), manifest, testLevel, testClasses |
sfdt_manifest_from_git | base (default main), head (default HEAD), package, name — build package.xml + destructiveChanges.xml from a git diff. Read-only. |
Deployment & rollback (safety-gated)
Modifying operations require confirmExecution: true as an explicit safety gate. Omit it (or
set it false) and the tool returns a validation error and aborts.
confirmExecution is not the control for path arguments. It authorises the operation,
while the model still supplies the argument — so since 0.23.1 every path-shaped argument is
contained independently of it:
file(sfdt_apex_run) andmanifest(sfdt_validate,sfdt_deploy) must be relative paths that resolve inside the project. Absolute paths and..segments are rejected.set(sfdt_data_export/_import/_load/_delete) must be a bare identifier matching^[A-Za-z0-9][A-Za-z0-9_-]*$— no dots, slashes, or leading-.
A rejected argument returns an error and the tool does not run. This exists because MCP arguments are chosen by a model, and the CLI’s AI surfaces feed that model untrusted org content — Apex compile errors, flow metadata, deploy failure text — so a path argument is untrusted input.
| Tool | Arguments |
|---|---|
sfdt_deploy | targetOrg (req), manifest, testLevel, testClasses, destructiveTiming, smart, deltaBase, deltaHead, dryRun, confirmExecution |
sfdt_quick_deploy | validationJobId (req), targetOrg (req), confirmExecution (req) |
sfdt_rollback | confirmExecution (req) |
sfdt_retrofit | source (req), target, metadata, execute; real deploys require confirmExecution |
sfdt_release | version, package (default all), name, confirmExecution (req) — builds the release manifest + notes and writes artifacts to the repo |
For sfdt_deploy, a validate-only run (dryRun: true) no longer requires confirmExecution; a
real deploy still does. See Smart Deploy.
Analysis & observe
| Tool | Arguments |
|---|---|
sfdt_drift | org (optional) |
sfdt_compare | source (req), target (req) |
sfdt_quality | generateStubs, fixPlan, apexGuru (org-side ApexGuru check only; license-gated, degrades to skipped), org (target for apexGuru) |
sfdt_logs | type — preflight | drift | deploy | rollback | quality |
sfdt_coverage | org (optional) — org-wide + per-class Apex coverage. Read-only. |
sfdt_scan | org (optional) — full metadata inventory of the org. Read-only. |
sfdt_dependencies | name (req) — component name; org (optional). What it references and what references it. Read-only. |
sfdt_flow_scan | org (optional) — Flow quality analysis via @sfdt/flow-core (fetches each active FlowDefinition version). Read-only. |
sfdt_docs | ai (boolean) — generate MkDocs project docs (objects/fields, Apex, Flows) with an optional AI overview and a Mermaid ER diagram. Read-only. |
sfdt_test | classNames (array, optional) — run Apex tests via the enhanced runner; limit to specific classes or use the configured set. Consumes org test resources; not metadata-mutating. |
sfdt_history | type (optional filter — audit | monitor | quality | test-run | deploy | agent-test …), limit (default 30) — recent runs from the local run index. Read-only. |
sfdt_soql_search | term, category (all | custom | standard), limit, org — find sObjects by name substring. Read-only. |
sfdt_soql_describe | sobject (req), filter, tooling, org — fields, picklists, references, child relationships. Read-only. |
sfdt_soql_validate | query (req), org — local checks + an org LIMIT 0 round-trip; never executes rows. Read-only. |
sfdt_soql_plan | query (req), org — org query plans via the REST explain endpoint; never executes the query. Read-only. |
sfdt_soql_query | query (req), limit, tooling, org — bounded SOQL execution (soql.defaultLimit/soql.maxLimit, never unbounded). Read-only. |
sfdt_apex_logs | org, logId (retrieve one log body), limit (default 20) — list recent Apex debug logs or fetch one by Id. Read-only. |
Scratch orgs & data (safety-gated)
The mutating tools here take the same confirmExecution: true gate as deployment. sfdt_data_export
and sfdt_scratch_pool with action: "status" are read-only and need no gate.
| Tool | Arguments |
|---|---|
sfdt_scratch_create | alias, days (1–30), confirmExecution (req) |
sfdt_scratch_delete | target (req — alias or username), confirmExecution (req) |
sfdt_scratch_pool | action (req — status read-only | fill mutating), size, confirmExecution (req for fill) |
sfdt_data_export | set (req), org — export a configured data set to local files. Read-only against the org. |
sfdt_data_import | set (req), org, confirmExecution (req) — write records to the org |
sfdt_data_delete | set (req), org, confirmExecution (req) — bulk-delete records in the org |
sfdt_apex_trace | action (req — list read-only | start/stop mutating), org, user, duration (minutes), debugLevel, all, confirmExecution (req for start/stop) — manage USER_DEBUG trace flags |
sfdt_apex_run | org, file (project-relative path) or apexCode (inline), confirmExecution (req) — execute Anonymous Apex in the org |
Org health & notifications
| Tool | Arguments |
|---|---|
sfdt_audit | check — any audit check id or all. Includes inactive-flows, unused-permsets, connected-apps, field-descriptions, apex-unreferenced, lint-access, inactive-validations, inactive-workflows, lint-access-fields |
sfdt_monitor | check — limits, errors, health, backup, org-info, deploy-history, deprecated-api, flow-errors, or all |
sfdt_notify | Push the latest audit/monitor snapshot to configured channels (type: audit | monitor) |
sfdt_pr_comment | type (audit | monitor), pr (a PR number in the current repository — a URL is refused, since a URL names a repository too), confirmExecution (req) — post the latest snapshot as a PR comment. The body is redacted before it leaves the machine. |
Context budget governance
| Tool | Arguments |
|---|---|
sfdt_get_parked_result | ref (req) — e.g. parked://<uuid> |
Context parking
When a tool result exceeds mcp.parking.thresholdBytes (default 50 KB), the server writes the
full payload to .sfdt/cache/parked/<uuid>.json and returns a lightweight envelope instead:
{
"_parked": true,
"ref": "parked://<uuid>",
"byteSize": 123456,
"rowCount": 42,
"preview": "...",
"ttlMs": 86400000,
"cacheScope": "session"
}The envelope carries SEP-2549 cache metadata — ttlMs + cacheScope (replacing the older
expiresAt) — aligned with the MCP 2026-07-28 release candidate. Treat ttlMs as relative to
when you received the envelope. Retrieve the full payload with sfdt_get_parked_result.
Purge expired parked files:
sfdt mcp cleanupClient setup
Claude Code
{
"mcpServers": {
"sfdt": {
"command": "node",
"args": ["/absolute/path/to/sfdt/bin/sfdt.js", "mcp", "start"]
}
}
}Cursor
- Settings → Features → MCP → + Add New MCP Server.
- Name:
sfdt; Type:stdio. - Command:
node /absolute/path/to/sfdt/bin/sfdt.js mcp start.