salesforce-metadata-mcp 2.12.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Production write guard
6
+
7
+ This server hands an LLM a Salesforce credential and lets the LLM decide what to do with it. Any
8
+ text the model reads along the way — a case comment, a field description, a retrieved `.flow` file,
9
+ an email body — can carry an instruction, and nothing downstream can distinguish it from one the
10
+ user typed. Salesforce's own hosted MCP servers ship a blunt version of the same defence: they
11
+ create and update records but refuse to delete, with deletes behind a separate opt-in server.
12
+
13
+ Nine tools are now refused against a **production** org: `sf_delete_metadata`, `sf_delete_record`,
14
+ `sf_bulk_delete_records`, `sf_execute_anonymous_apex`, `sf_uninstall_package`, `sf_create_user`,
15
+ `sf_update_user`, `sf_reset_user_password`, `sf_freeze_user`. Set `SF_PRODUCTION_GUARD=strict` to
16
+ refuse every write instead, or `=off` to disable the guard.
17
+
18
+ **Metadata creation is deliberately untouched.** All 137 `sf_create_*` tools, `sf_deploy_metadata`
19
+ and `sf_retrieve_metadata` still work against production. `sf_deploy_metadata` was reviewed and
20
+ left open because it has no `destructiveChanges` path — it builds a package.xml from the components
21
+ passed to it and can only add or upsert. A guard that taxed metadata authoring would be turned off
22
+ on day one, which is worse than no guard because it would then be off for everything too.
23
+
24
+ Apex authoring (`sf_create_apex_class`, `sf_create_apex_trigger`) is also left open, even though a
25
+ trigger is arbitrary code running on every DML. The line drawn is auditability: created Apex is
26
+ metadata with a name, an author and a deploy record, and can be found and removed afterwards.
27
+ `sf_execute_anonymous_apex` leaves no artifact at all, which is why it is the Apex path that is
28
+ blocked. This is a real residual risk and is documented rather than papered over.
29
+
30
+ An org counts as production only when it is not a sandbox, not a Developer Edition org, and has no
31
+ trial expiry. **`IsSandbox = false` alone is not sufficient** — Developer Edition and scratch orgs
32
+ both report `false`, and gating those would have made the feature unusable for everyone developing
33
+ against a dev org. Org identity is resolved once per process and cached; if it cannot be determined,
34
+ the org is treated as production (fail closed).
35
+
36
+ The guard is a hard refusal rather than a confirmation prompt, because a prompt the calling agent
37
+ can satisfy by itself is not a control — and this server is routinely run under clients with
38
+ permissions bypassed. Only an environment variable set outside the conversation lifts it.
39
+
40
+ Implemented in `src/services/guard.ts` and wired through the single `ToolsetRegistry.capture()`
41
+ proxy, so every current and future tool passes through it without the 33 tool modules changing.
42
+ Covered by `qa-guard.mjs` (26 checks), including assertions that each metadata-creation tool stays
43
+ allowed and that Developer Edition / scratch / sandbox orgs are never treated as production.
44
+
45
+ ### Security: sf_deploy_metadata could be turned into a metadata deletion primitive
46
+
47
+ Found by attacking the guard above rather than in review, and it was live in every published version
48
+ that shipped `sf_deploy_metadata` with the `componentsXml` parameter.
49
+
50
+ `inferMetadataPath`'s `default` branch (unrecognised metadata types) returned `` `${lower}s/${name}` ``
51
+ with no extension appended, so the caller-supplied `name` controlled the tail of the zip path
52
+ outright. A component of `{ type: "X", name: "../destructiveChanges.xml" }` produced the zip path
53
+ `xs/../destructiveChanges.xml`, which JSZip normalises to a **root-level `destructiveChanges.xml`** —
54
+ the manifest the Metadata API uses to *delete* every component listed in it. Confirmed by reading the
55
+ generated archive's entry list, not by inspection.
56
+
57
+ Impact: `sf_deploy_metadata` was documented and treated as additive-only (it is not in the production
58
+ guard's blocked set for exactly that reason), while in fact being able to delete arbitrary metadata
59
+ from any org the server could reach — bypassing the block on `sf_delete_metadata` completely.
60
+
61
+ Fixed by validating component names before they reach the zip: path separators and `..` are rejected,
62
+ as are the reserved manifest names `package.xml`, `destructiveChanges.xml`,
63
+ `destructiveChangesPre.xml` and `destructiveChangesPost.xml` (case-insensitively). The assembled path
64
+ is re-checked afterwards so a future branch that builds a path some other way cannot reintroduce this.
65
+ Salesforce component names cannot contain path separators in any metadata type, so nothing legitimate
66
+ is rejected. `sf_deploy_metadata` therefore stays available against production, as intended.
67
+
68
+ ### Security: production guard evaluated the wrong org for tools taking an org override
69
+
70
+ `uninstallPackage()` ignores its `auth` argument entirely and shells out to
71
+ `sf package uninstall --target-org <params.targetOrg>`. The guard resolved production-ness from
72
+ `getAuth()` — a different org — so pointing `SF_INSTANCE_URL` at a dev org and passing
73
+ `targetOrg: "<prod-alias>"` walked a gated tool straight past a guard that believed it was protecting
74
+ production.
75
+
76
+ Gated tools called with an explicit `targetOrg`, `targetAlias`, `targetOrgAlias` or `orgAlias` are now
77
+ refused outright, since the named org cannot be verified from here without a second CLI round-trip.
78
+ Fail closed rather than guess.
79
+
80
+ ### Security: package.xml manifest injection
81
+
82
+ `buildPackageXml` interpolated component names and types into XML unescaped, so a name containing `<`
83
+ closed the element early and appended attacker-chosen manifest entries. Lower severity than the two
84
+ above (a manifest cannot express deletion, and listed components must also be present in the zip), but
85
+ fixed with proper escaping. The `*` wildcard is unaffected.
86
+
87
+ Attack coverage for all three lives in `qa-guard.mjs` (43 checks) so they cannot regress silently.
88
+
89
+
3
90
  ## [2.12.0] - 2026-08-11
4
91
 
5
92
  ### Five new tools (223 → 228), and a dangerous pair of functions removed
package/README.md CHANGED
@@ -45,6 +45,72 @@ Add to your MCP configuration (`claude_desktop_config.json` or `.claude/settings
45
45
 
46
46
  See [SETUP.md](SETUP.md) for all authentication methods and detailed setup instructions.
47
47
 
48
+ ### One-click install (Claude Desktop)
49
+
50
+ Download `salesforce-metadata-mcp-<version>.mcpb` from the [latest release](https://github.com/semwalajay83-sem/salesforce-metadata-mcp/releases/latest), then drag it into **Claude Desktop → Settings → Extensions**. It prompts for your org URL and credentials — no JSON editing.
51
+
52
+ The bundle resolves the server from npm at launch rather than embedding a copy, so it always runs the current published version and never needs re-downloading after an upgrade.
53
+
54
+ ### Docker
55
+
56
+ ```bash
57
+ docker build -t salesforce-metadata-mcp .
58
+ docker run -i --rm \
59
+ -e SF_INSTANCE_URL="https://your-org.my.salesforce.com" \
60
+ -e SF_ACCESS_TOKEN="your_access_token" \
61
+ salesforce-metadata-mcp
62
+ ```
63
+
64
+ The image builds from source, runs as a non-root user, and ships production dependencies only. Because this server speaks MCP over stdio, `-i` is required — the container is driven by its client, not run as a background service. `SF_ALIAS` will not work in a container: the Salesforce CLI's login flow needs a browser, so use token, JWT, refresh-token, or client-credentials variables instead.
65
+
66
+ ---
67
+
68
+ ## Toolsets — loading 228 tools without burning your context
69
+
70
+ All 228 tools are always available. Most are not loaded into the model's context until something asks for them.
71
+
72
+ Listing every tool up front costs roughly **98,000 tokens** — about half a 200k context window, spent
73
+ before you type anything, whether or not the session ever touches OmniStudio or DevOps Center. A
74
+ 228-candidate tool list also makes the model measurably worse at picking the right tool. So the server
75
+ starts with a small core loaded and pulls in the rest on demand:
76
+
77
+ | Startup | Tools listed | Approx. tokens |
78
+ |---------|-------------:|---------------:|
79
+ | Default (`core,metadata`) | 18 | **~9,400** |
80
+ | After loading two more toolsets | 41 | ~20,300 |
81
+ | `SF_TOOLSETS=all` | 231 | ~98,600 |
82
+
83
+ The default covers what nearly every session needs: describe/list objects, SOQL query,
84
+ deploy/retrieve/delete metadata, deploy status, and core schema creation (objects, fields, formula
85
+ fields, picklist values, validation rules, approval processes).
86
+
87
+ Three tools are always present and make everything else reachable:
88
+
89
+ - **`sf_find_tool`** — search all 228 tools by name and load whatever contains the matches, in one
90
+ call. Ask for *"create an omniscript"* and it finds the tools, loads `omnistudio`, and they are
91
+ callable immediately. This is usually all you or the model needs.
92
+ - **`sf_load_toolset`** — load named toolsets explicitly.
93
+ - **`sf_list_toolsets`** — browse all toolsets, their tool counts, and what is loaded.
94
+
95
+ In practice you don't manage this by hand: ask for what you want, and the model loads what it needs.
96
+
97
+ To restore the previous behaviour of loading everything at startup, set `SF_TOOLSETS=all`. To start
98
+ with only the three meta-tools, set `SF_TOOLSETS=none`. To pick your own core, pass a list:
99
+
100
+ ```json
101
+ { "env": { "SF_TOOLSETS": "metadata,objects,automation,security" } }
102
+ ```
103
+
104
+ Available toolsets: `core`, `metadata`, `objects`, `data`, `flows`, `automation`, `security`, `apex`,
105
+ `lwc`, `ui`, `pages`, `actions`, `agentforce`, `omnistudio`, `omnichannel`, `devops`, `deployment`,
106
+ `integrations`, `identity`, `reports`, `experience`, `admin`, `monitoring`, `audit`, `einstein`,
107
+ `knowledge`, `cpq`, `sandbox`, `streaming`, `visualforce`, `aura`, `comms`, `mcp`, `i18n`.
108
+
109
+ Flow tools live in their own `flows` toolset because `sf_create_flow` carries the full Flow element
110
+ schema — 15,266 bytes (~4,126 tokens) on its own, the largest tool definition in the server. Keeping
111
+ it out of the default means sessions that never build a Flow never pay for it, while sessions that do
112
+ still get the complete validated schema.
113
+
48
114
  ---
49
115
 
50
116
  ## Tools — 228 total
@@ -210,6 +276,49 @@ Highlights below; see [TOOLS.md](TOOLS.md) for the complete reference with param
210
276
  | `SF_ACCESS_TOKEN` | Static access token (expires ~1hr) | For static |
211
277
  | `PORT` | HTTP server port (default: 3000) | For HTTP mode |
212
278
  | `TRANSPORT` | `stdio` or `http` (default: stdio) | Optional |
279
+ | `SF_TOOLSETS` | Toolsets to load at startup: `all`, `none`, or a comma-separated list (default: `core,metadata`) | Optional |
280
+ | `SF_TOOLSETS_VERBOSE` | Set to `1` to print the full toolset list to stderr on startup | Optional |
281
+ | `SF_PRODUCTION_GUARD` | `destructive` (default), `strict`, or `off` — see below | Optional |
282
+
283
+ ---
284
+
285
+ ## Production write guard
286
+
287
+ This server hands an LLM a Salesforce credential, and the LLM decides what to do with it. Any text
288
+ the model reads on the way — a case comment, a field description, a retrieved `.flow` file, an
289
+ email body — can carry an instruction, and nothing downstream can tell it apart from one you typed.
290
+
291
+ So against a **production org**, nine tools are refused by default:
292
+
293
+ | Blocked on production | Why |
294
+ |---|---|
295
+ | `sf_delete_metadata` | Destroys metadata and every record in it |
296
+ | `sf_delete_record`, `sf_bulk_delete_records` | Destroy data |
297
+ | `sf_execute_anonymous_apex` | Arbitrary code that leaves no artifact behind |
298
+ | `sf_uninstall_package` | Removes a managed package and its data |
299
+ | `sf_create_user`, `sf_update_user` | Privilege escalation |
300
+ | `sf_reset_user_password`, `sf_freeze_user` | Account takeover / lockout |
301
+
302
+ **Metadata creation is not affected.** All 137 `sf_create_*` tools, `sf_deploy_metadata` and
303
+ `sf_retrieve_metadata` work against production exactly as before — authoring metadata by natural
304
+ language is the point of this package, and a guard that taxed it would just get switched off.
305
+
306
+ Apex authoring (`sf_create_apex_class`, `sf_create_apex_trigger`) is also **not** blocked, even
307
+ though a trigger runs on every DML. The line drawn is auditability: created Apex is metadata — it
308
+ has a name, an author and a deploy record, and can be found and removed. `sf_execute_anonymous_apex`
309
+ leaves nothing to find, which is why that one is blocked.
310
+
311
+ An org counts as production only when it is **not** a sandbox, **not** a Developer Edition org, and
312
+ **not** on a trial/scratch expiry. Sandboxes, dev orgs and scratch orgs are never gated.
313
+
314
+ ```jsonc
315
+ { "env": { "SF_PRODUCTION_GUARD": "strict" } } // refuse ALL writes on production
316
+ { "env": { "SF_PRODUCTION_GUARD": "off" } } // no guard at all
317
+ ```
318
+
319
+ The guard is a hard refusal, not a confirmation prompt: a prompt the calling agent can approve by
320
+ itself is not a control, and this server is often run with client permissions bypassed. Only the
321
+ environment variable lifts it.
213
322
 
214
323
  ---
215
324
 
package/SECURITY.md CHANGED
@@ -68,11 +68,31 @@ All tool inputs are validated by Zod schemas before being used:
68
68
 
69
69
  ## Supported Versions
70
70
 
71
- | Version | Supported |
72
- |---------|-----------|
73
- | 2.1.x | ✅ Current |
74
- | 2.0.x | ⚠️ Upgrade recommended |
75
- | 1.x | No longer supported |
71
+ | Version | Status |
72
+ |---------|--------|
73
+ | 2.12.x | ✅ Supported — current npm `latest` |
74
+ | 2.11.2 | Contains all security fixes |
75
+ | 2.11.1 | ⚠️ Has the code fixes, but ships 5 production-dependency advisories resolved in 2.11.2 — upgrade |
76
+ | **≤ 2.8.7** | ❌ **Deprecated on npm (2026-08-26) — command injection (RCE), SOQL injection, credential leak** |
77
+ | 1.x | ❌ No longer supported |
78
+
79
+ ### Deprecated versions — 2026-08-26
80
+
81
+ Every version **2.0.0 through 2.8.7** was deprecated on npm and now emits a warning on install.
82
+
83
+ The security audit in **v2.8.8** fixed a confirmed command injection (RCE), a SOQL injection, a
84
+ credential leak and a generated-code injection. **v2.8.8, v2.8.9, v2.9.0, v2.10.0 and v2.11.0 were
85
+ never published to npm**, so the first npm release carrying those fixes is **v2.11.1**. Anyone on
86
+ `2.8.7` or below — including the version that was npm `latest` at the time — is running unfixed code.
87
+
88
+ If you are pinned to any version at or below 2.8.7, upgrade:
89
+
90
+ ```
91
+ npm install salesforce-metadata-mcp@latest
92
+ ```
93
+
94
+ Note that pinning to an exact old version bypasses `latest` entirely; check your lockfile, Dockerfile
95
+ or MCP client config for a hardcoded version string.
76
96
 
77
97
  ---
78
98