Skip to main content
Glama
tsmztech

salesforce-remote-mcp-cloudflare

by tsmztech
README.md
# salesforce-remote-mcp-cloudflare

A **remote MCP (Model Context Protocol) server for Salesforce**, deployable on
**Cloudflare Workers** as a **Claude custom connector**. It exposes the same 15
Salesforce tools as the local stdio package
[`@tsmztech/mcp-server-salesforce`](https://github.com/tsmztech/mcp-server-salesforce) —
SOQL/SOSL queries, DML, object/field/metadata management, Apex read/write/execute,
debug logs — over Streamable HTTP with full per-user OAuth.

This is a **self-hosted template, not a hosted service**. You (the setup admin) create
a Salesforce Connected App in your org, deploy your own Worker with your own
credentials, and register your Worker's URL in Claude. Nothing is shared with anyone
else, and no credentials ever leave your Cloudflare account and your Salesforce org.

## How it works

```
Claude (web/desktop/mobile)
   │  OAuth 2.1 ─ handled automatically (Dynamic Client Registration, PKCE)
   ▼
Your Cloudflare Worker  ←  this repo
   │  "Login with Salesforce" ─ your Connected App (OAuth Web Server flow + PKCE)
   ▼
Your Salesforce org
```

There are **two separate OAuth relationships**:

1. **Claude ↔ Worker** — handled entirely by
   [`@cloudflare/workers-oauth-provider`](https://github.com/cloudflare/workers-oauth-provider).
   You configure **nothing** for this; Claude registers itself automatically.
2. **Worker ↔ Salesforce** — your Connected App. Its consumer key/secret live only in
   Worker secrets.

When a user clicks **Connect** in Claude, they are redirected to *your org's* Salesforce
login page. Their Salesforce tokens are stored encrypted inside the OAuth grant and are
never visible to Claude or to other users. **Every tool call runs as the connected
user** — their profile, permissions, and field-level security apply. Expired access
tokens are refreshed automatically (proactively when Claude refreshes its own token,
and reactively on `INVALID_SESSION_ID`).

**One deployment = one Salesforce org.** The org (production, sandbox, or a My Domain)
is fixed by your configuration. End users don't pick an org — they just log in with
their own identity in that org. To serve a second org, deploy a second Worker.

## Prerequisites

- A **Cloudflare account** (free tier works) — [sign up](https://dash.cloudflare.com/sign-up)
- **Node.js 18+** and npm
- A **Salesforce org** you administer (production, Developer Edition, or sandbox)
- A **Claude** plan that supports custom connectors (Pro, Max, Team, or Enterprise)

## Step 1 — Deploy the Worker

Deploy first: you need your Worker's URL before you can create the Connected App
(its OAuth callback points at the Worker).

```bash
git clone <this-repo>
cd salesforce-remote-mcp-cloudflare
npm install --legacy-peer-deps
npx wrangler login
```

Create the KV namespace the OAuth provider stores its (encrypted) state in:

```bash
npx wrangler kv namespace create OAUTH_KV
```

Copy the printed `id` into [wrangler.jsonc](wrangler.jsonc):

```jsonc
"kv_namespaces": [{ "binding": "OAUTH_KV", "id": "<your-id-here>" }],
```

In the same file, set your org's login host — this pins the deployment to your org:

```jsonc
// production / Developer Edition:  login.salesforce.com
// sandbox:                         test.salesforce.com
// My Domain (recommended):         yourcompany.my.salesforce.com
"vars": { "SALESFORCE_LOGIN_HOST": "login.salesforce.com" }
```

Set the state-encryption key (any long random value; you never need it again):

```bash
openssl rand -base64 32 | npx wrangler secret put COOKIE_ENCRYPTION_KEY
```

Deploy:

```bash
npx wrangler deploy
```

Note the URL wrangler prints, e.g.
`https://salesforce-remote-mcp-cloudflare.<your-subdomain>.workers.dev`.
That is **your Worker URL** — you'll use it in Steps 2 and 4. (The server won't work
yet; it still needs the Connected App credentials from the next step.)

## Step 2 — Create the Connected App in Salesforce

> Connected Apps can only be created through the Setup UI — the Metadata API blocks
> programmatic creation, so there is no CLI shortcut for this step.

1. In your org: **Setup → App Manager → New Connected App**
   (in some orgs: **New Connected App → Create a Connected App**).
2. Fill in the basics: any name (e.g. `Claude MCP`), your email.
3. Check **Enable OAuth Settings** and configure:
   - **Callback URL**:
     `https://<your-worker-url>/sf/callback`
     (e.g. `https://salesforce-remote-mcp-cloudflare.acme.workers.dev/sf/callback`)
   - **Selected OAuth Scopes** — add exactly these two:
     - *Manage user data via APIs (api)*
     - *Perform requests at any time (refresh_token, offline_access)*
   - **Require Proof Key for Code Exchange (PKCE)**: leave enabled (the server uses PKCE).
   - **Require Secret for Web Server Flow**: leave enabled.
4. **Save**. Salesforce warns changes can take **up to 10 minutes** to take effect.
5. Open the app's detail page → **Manage Consumer Details** (email verification may be
   required) and copy the **Consumer Key** and **Consumer Secret**.

Optional hardening (Setup → Connected Apps → Manage → Edit Policies): set
**Permitted Users** to *Admin approved users are pre-authorized* to allowlist which
profiles/permission sets may use the connector at all.

## Step 3 — Give the Worker its Salesforce credentials

```bash
npx wrangler secret put SALESFORCE_CLIENT_ID       # paste the Consumer Key
npx wrangler secret put SALESFORCE_CLIENT_SECRET   # paste the Consumer Secret
```

Secrets are stored by Cloudflare, never in code or git. That's the whole server-side
setup — no redeploy needed after setting secrets.

## Step 4 — Add the connector in Claude

- **Team / Enterprise**: an org owner/admin adds it once for everyone —
  **Admin settings → Connectors → Add custom connector**.
- **Pro / Max**: add it for yourself — **Settings → Connectors → Add custom connector**.

In the dialog:

- **Name**: anything, e.g. `Salesforce`
- **Remote MCP server URL**: `https://<your-worker-url>/mcp` — note the **`/mcp`** path
- **Advanced settings → OAuth Client ID / Client Secret: LEAVE BLANK.**
  Those fields are a fallback for servers that can't register clients automatically;
  this server supports Dynamic Client Registration, so Claude configures itself.
  Your Connected App's key/secret belong only in Worker secrets (Step 3) — never
  paste them into Claude.

## Step 5 — Connect (end users)

1. In Claude, find the connector and click **Connect**.
2. You're redirected to your company's Salesforce login — log in and click **Allow**.
3. Done. Ask Claude to `ping` the Salesforce connector to confirm: it replies with the
   Salesforce username the session is bound to.

Each user connects individually and gets their own permissions — what a user can't see
or edit in Salesforce, they can't see or edit through Claude.

## The tools

| Category | Tools |
|---|---|
| Discovery | `salesforce_search_objects`, `salesforce_describe_object` |
| Query | `salesforce_query_records`, `salesforce_aggregate_query`, `salesforce_search_all` (SOSL) |
| Data | `salesforce_dml_records` (insert/update/delete/upsert) |
| Schema | `salesforce_manage_object`, `salesforce_manage_field`, `salesforce_manage_field_permissions` |
| Apex | `salesforce_read_apex`, `salesforce_write_apex`, `salesforce_read_apex_trigger`, `salesforce_write_apex_trigger`, `salesforce_execute_anonymous` |
| Debug | `salesforce_manage_debug_logs` |
| Health | `ping` (no Salesforce call; shows the bound user) |

Tool behavior matches the local `@tsmztech/mcp-server-salesforce` package. Large
results are truncated at ~140k characters (Claude's connector limit is ~150k) with a
notice asking to narrow the query.

## Troubleshooting

| Symptom | Likely cause / fix |
|---|---|
| Claude: "could not reach server" when adding the connector | URL must end in `/mcp`. The Worker must be deployed (`npx wrangler deploy`). |
| Salesforce error page during Connect: `redirect_uri_mismatch` | Connected App callback URL doesn't exactly match `https://<worker-url>/sf/callback`. Remember changes take up to 10 min. |
| `invalid_client_id` / `invalid_client` during Connect | `SALESFORCE_CLIENT_ID`/`SECRET` secrets missing or wrong (re-run Step 3), or the Connected App hasn't propagated yet. |
| Login page is the wrong org type (prod vs sandbox) | Fix `SALESFORCE_LOGIN_HOST` in `wrangler.jsonc` and redeploy. |
| Tool calls fail with "reconnect" message | The refresh token was revoked or expired (e.g. admin revoked OAuth usage, or org token policies). Disconnect and reconnect the connector. |
| `INVALID_SESSION_ID` errors surfacing to the user | Shouldn't happen — the server auto-refreshes once and retries. If persistent, check the Connected App's refresh-token policy (*Refresh token is valid until revoked* is the friendliest). |
| Worker logs | `npx wrangler tail` streams live logs (no tokens are ever logged). |
| Corporate IP allowlisting | Claude's egress range is `160.79.104.0/21` — relevant if your org enforces login IP ranges on the Connected App. |

## Security notes

- **Per-user identity**: every Salesforce call uses the connected user's own token;
  org permissions and FLS always apply. There is no service account.
- **No token passthrough**: Claude's token and Salesforce's tokens are separate;
  neither is ever forwarded to the other side.
- **Storage**: Salesforce tokens are stored encrypted at rest inside the OAuth grant
  (Cloudflare KV, encrypted by `workers-oauth-provider`); the interim login state is
  AES-GCM sealed with `COOKIE_ENCRYPTION_KEY`.
- **Secrets**: only ever via `wrangler secret put` — never in code, config, or git.
- **Revocation**: a Salesforce admin can cut any user (or the whole app) off at any
  time — Setup → Connected Apps OAuth Usage → Revoke. Users can also disconnect from
  Claude's connector settings.
- See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.

## Local development

```bash
cp .dev.vars.example .dev.vars   # fill in your Connected App creds (gitignored)
npx wrangler dev                 # http://localhost:8787
npm run typecheck
npm test
```

Note: connecting from claude.ai requires a public HTTPS URL, so end-to-end connector
testing happens against a deployed Worker; `wrangler dev` is for the OAuth endpoints
and unit-testable logic.

## License

[MIT](LICENSE)