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 +87 -0
- package/README.md +109 -0
- package/SECURITY.md +25 -5
- package/TOOLS.md +326 -326
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/dist/schemas/index.d.ts +1097 -1097
- package/dist/services/deployment.d.ts.map +1 -1
- package/dist/services/deployment.js +60 -2
- package/dist/services/deployment.js.map +1 -1
- package/dist/services/guard.d.ts +45 -0
- package/dist/services/guard.d.ts.map +1 -0
- package/dist/services/guard.js +177 -0
- package/dist/services/guard.js.map +1 -0
- package/dist/services/salesforce.d.ts.map +1 -1
- package/dist/services/salesforce.js +54 -60
- package/dist/services/salesforce.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +47 -33
- package/dist/tools/index.js.map +1 -1
- package/dist/toolsets.d.ts +106 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +374 -0
- package/dist/toolsets.js.map +1 -0
- package/package.json +6 -2
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 |
|
|
72
|
-
|
|
73
|
-
| 2.
|
|
74
|
-
| 2.
|
|
75
|
-
| 1
|
|
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
|
|