Skip to main content
← Back to Projects
Salesforce Prototype Flagship Salesforce + Claude demo app

Headless 360 AI Assistant

Claude talks to Salesforce as the signed-in user, and asks before every write

A local Next.js chat app where a Salesforce user asks questions in plain language; Claude calls Salesforce hosted MCP servers and asks before every write.

Solo build; the capstone of my Headless 360 series

SalesforceMCPClaudeApexNext.jsAI Agents
01 / Problem

The problem

A Salesforce user often wants a quick answer or a small change without opening the Salesforce UI: "How healthy is the Acme account?", "Create a follow-up task for Friday." An AI assistant can do that, but the usual shortcut is to give it one broad integration user. Then the assistant sees every record that user can see, not what the person asking is allowed to see, and a write happens the moment the model decides to make one.

I wanted the opposite. The assistant should only ever see and change what the signed-in person could see and change in Salesforce. And it should never write anything until that person has read the exact change and said yes. Salesforce now publishes hosted MCP servers, and Anthropic's API can call MCP servers directly, so I built a small app to see how far those two pieces go when the rules above are not negotiable.

02 / Solution

What I built

A local Next.js chat app. You sign in with your own Salesforce account and ask questions in plain language. The server sends each turn to the Claude Messages API with Anthropic's MCP connector, and Claude calls Salesforce's hosted MCP servers directly with your access token. Reads just happen. A write shows up as an approval card with a one-sentence summary, and only runs after you click Approve.

There is a second workflow at /brief: a read-only meeting prep brief for one Account, with fixed sections and cited record Ids.

The one-sentence version: Claude works in your org as you, and it asks before it changes anything.

It is the capstone of my 15-day Headless 360 series (coming soon). Day 15 (coming soon) builds the assistant step by step and Day 10 (coming soon) builds the brief.

03 / Demo

See it running

Demo coming

A recorded walkthrough of Headless 360 AI Assistant is on the way. Until then, the write-up and the diagram below show how it works.

04 / Architecture

How it fits together

Headless 360 AI Assistant

Claude works in Salesforce as the signed-in user

  1. BrowserChat, tool trace and the approval cardApprove / Rejectbrief at /brief
    message / Approve
  2. Next.js serverRuns locally: sign-in, sealed token cookie, request builderOAuth + PKCEwrite tools offwrite audit
    request + user's token
  3. Claude APIAnthropic's MCP connector makes the tool callsMessages APIpropose_write_action
    tools/call as the user
  4. Salesforce MCPHosted MCP servers in the user's orgsobject-allcustom server (optional)
    custom tools
  5. Apex toolsInvocable Apex, with sharing, user modegetAccountHealthcreateFollowUpTask
Claude calls Salesforce's hosted MCP servers with the signed-in user's token; write tools stay off until the user approves one change.
05 / Write-up

Architecture

Five parts, left to right. The browser shows the chat, the tool trace and the approval card. The Next.js server handles sign-in, keeps your Salesforce tokens in a sealed cookie and builds each Claude request. Claude, through the MCP connector, does the tool calling. On the Salesforce side there are two hosted MCP servers: the standard sobject-all server and an optional custom server that publishes my two Apex classes as tools.

The app has no MCP client of its own. That is a deliberate trade-off. It keeps the server small, but it also means the tool calls execute inside Anthropic's API, so my code can't stop a single call halfway. The README says this openly. The guard is therefore the request itself: on every normal turn the write tools are switched off in the toolset config, and Claude can only propose a write through a client tool called propose_write_action. That tool ends the turn. When you approve, the next request enables only the one tool you approved, and afterwards the server compares each executed write with the approved arguments and reports the match in the trace.

Sign-in uses OAuth 2.0 with PKCE through an External Client App. Your access token goes to the connector as authorization_token on each MCP server, so Salesforce enforces your permissions, field-level security and sharing on every call. The Apex tools add their own layer: they run with sharing and query in user mode.

Stack

  • Next.js and TypeScript (App Router): one small server for the OAuth routes, the streaming chat route and the brief.
  • Claude Messages API with the MCP connector (beta), default model Claude Sonnet 5.5: Claude calls the Salesforce servers directly, so I don't run an MCP client.
  • Salesforce hosted MCP servers: sobject-all for standard record access, plus a custom server for my own tools.
  • Apex and an autolaunched Flow: business logic the model calls instead of inventing it, deployed from an SFDX project at API version 67.0.
  • OAuth 2.0 with PKCE: each user signs in as themselves; no shared integration user.
  • Vitest: 61 test cases for the web app, with Salesforce and Anthropic mocked. The Apex has 15 test methods, all passing in the org (98% and 96% coverage).

Key features

  • Runs as you. Every tool call carries the signed-in user's token, so the answer depends on what that user can see.
  • Approve before every write. The approval card shows the tool, its arguments and a plain summary. The chat input stays blocked until you approve or reject. If you type a new message instead, the proposal counts as not approved.
  • Write audit. After an approved turn the server checks each executed write against what you approved, and the trace says whether it matched.
  • Tool trace. Each MCP call shows server, tool, input and output, along with token refreshes, thinking summaries and approvals.
  • Account health tool. getAccountHealth returns pipeline, deals closing in 30 days, open and high-priority cases, days since the last activity, a 0–100 score and a status: Healthy, Watch or At Risk.
  • Meeting prep brief. Read-only by construction: every write tool is disabled and the proposal tool is not sent. It writes "not found" instead of guessing.

Code or config highlight

This is the whole approval guard on the request side, from web/lib/anthropic.ts:

/**
 * One toolset per server, fail closed: every tool starts disabled (default_config), then only the
 * configured read tools are enabled, plus the single write tool the user approved. A tool the
 * server adds or renames later (for example one re-added in Setup, which gets a generated name)
 * stays disabled until it is listed in the read or write tools.
 */
export function buildMcpToolsets(config: AppConfig, enabledWrite?: { server: string; tool: string }): BetaMCPToolset[] {
  return serverNames(config).map((server) => {
    const configs: Record<string, BetaMCPToolConfig> = {};
    for (const tool of readToolsFor(config, server)) configs[tool] = { enabled: true };
    if (enabledWrite?.server === server && writeToolsFor(config, server).includes(enabledWrite.tool)) {
      configs[enabledWrite.tool] = { enabled: true };
    }
    return { type: "mcp_toolset", mcp_server_name: server, default_config: { enabled: false }, configs };
  });
}

It is an allowlist, built fresh for every request. On a normal turn only the listed read tools are on, so questions need no approval and every write tool is off. On the approval request exactly one write tool comes back on.

Results

Honest status first: this is a single-user local demo. On 4 October 2026 I ran it end to end against a Salesforce Developer Edition org: sign-in through the External Client App, reads on the standard sobject-all server, the two custom Apex tools on the custom server with the same access token, an approved write and a rejected one, a token refresh after 16 idle minutes, sign-out with token revocation, and the meeting brief (about 30 seconds for a full brief; "not found" for an Account that doesn't exist).

The test found three bugs, all fixed the same day: a tool re-added in Setup gets a generated name, which slipped past the original write-tool denylist (now an allowlist that fails closed); Claude's first proposal guessed a write tool's argument names because disabled tools' schemas are hidden (the system prompt now names them); and the Sign out button was refused by the app's own same-origin check because of a strict referrer policy (fixed, and the token is revoked again).

What exists today: the local app with the chat, the approval flow and the brief, 61 web tests and 15 Apex test methods, and a source list for every API shape it relies on. There is no live deployment and none is planned as is: it has no rate limiting or multi-user hardening. No usage numbers are tracked.

Lessons

The first lesson was about where the guard can live. An approval step usually means intercepting tool calls, but with the MCP connector the calls run on Anthropic's side, so there is nothing to intercept. Writing the guard as per-request tool configuration, plus an audit afterwards, is simpler and easier to explain, as long as the limitation is stated plainly.

The second was to treat record data as data. The system prompt tells Claude that tool output is not instructions, because a Case description can contain anything.

The third was about retries. Agents retry, so the follow-up task tool returns the existing open task instead of creating a duplicate when the record, subject and due date match. What I would do differently: run the real-org test earlier. Setup paths, URL forms and tool names are exactly the details a mocked test can't confirm, and the org test changed several of them.

Links

The code is in the salesforce-headless-360 repository (MIT licence). The series covers the build in detail: start with Day 15 (coming soon), then Day 12 (coming soon) for the custom Apex tools and Day 11 (coming soon) for the security model. For the client side of my Salesforce work, see Salesforce client work.

What's next: run the org test, record a demo with seeded demo data, and publish the repository.

Work with me

Need something like this built?

I build AI automations, agents and Salesforce solutions for teams. Tell me what you want to automate and I’ll tell you honestly whether it’s a fit.

Share
Discussion

Comments

Loading comments...

Add a comment

Comments are reviewed before they appear.