Skip to main content
← Back to Blog
2026-10-09 · 18 min readSalesforceAI Agents

Day 12/15 — Custom MCP Tools + Apex/Flow Business Logic

Turn invocable Apex and an autolaunched Flow into custom MCP tools on a Salesforce hosted server, write their descriptions for the model, test them as a restricted user, and let Claude Code and the Salesforce DX MCP Server do the busywork.

Avnish Yadav
Avnish Yadav
Developer & Automation Builder
Day 12/15 — Custom MCP Tools + Apex/Flow Business Logic

Neha asks her assistant: "How healthy is the Acme Global Tech account?" With only sobject-all connected, Claude does what a clever new hire would do. It runs a few SOQL queries, counts opportunities and cases, and makes up a scoring rule on the spot. The answer sounds reasonable. Ask again tomorrow and the rule may differ, and it will never match the health score sales operations agreed on last quarter.

Arjun wants one definition of account health, written once, tested, permission-checked, and called the same way by a Flow or an AI agent, and follow-up tasks with validated fields and no duplicates when an agent retries.

That is the job of custom Model Context Protocol (MCP) tools. By the end of today you will have two invocable Apex actions and an autolaunched Flow published as tools on a custom hosted MCP server, tested as a restricted user, and called from Claude Code.

On Day 11 we built the security layers around an AI client. Today we move from what the agent may do to what it should do, on the hosted MCP servers of Salesforce Headless 360 (now called AIforce). The code comes from the companion repository, salesforce-headless-360.

Why custom tools

The standard hosted servers are generic on purpose, and you can't change their tool sets. That is right for "show me the open cases on Acme" and wrong for "is Acme healthy?", because the answer depends on a rule that lives in your organization, not in the data. A custom tool moves that rule out of the prompt and into code you control:

  • Consistency. Salesforce's FAQ says the server-side processing "is completely deterministic": "The LLM is entirely on the client side". Put the score in Apex and the model only decides when to call it, not how to calculate it.
  • Least privilege. A tool that can only create follow-up tasks on five object types, after validating every field, is a much smaller grant than a generic create tool on every object.
  • Smaller results. One call returns one self-contained answer instead of four raw query results to stitch together.

Salesforce's design advice: "The tool name and description you provide for each custom tool are as important as the backing implementation", and "The test is whether a tool call produces a self-contained, useful result." "Get the health of this account" passes. Keep the count small too: "Beyond a few dozen tools, an AI client struggles to select the right one for a given task." Custom servers can also "Combine tools from multiple standard servers under a single URL", which makes a custom server a curated tool belt.

Four ways to expose logic

Four backing types (invocable Apex, @AuraEnabled, @RestResource and autolaunched Flow) become tools on a custom hosted MCP server, which an MCP client calls as the signed-in user
Whatever backs the tool, the call runs as the signed-in user.

Custom servers can publish Apex invocable actions, @AuraEnabled methods, Apex REST endpoints, Flows, API Catalog endpoints and, per the Summer '26 guide, Named Query API, Prompt Builder prompts and Agentforce agents. For logic you write yourself, four options matter.

Pattern What you write Where the tool's schema comes from Choose it when Watch out for
@InvocableMethod (Apex action) A global class with a global static list-in, list-out method "The Apex method's input and output variables define the tool's parameter schema" New logic for agents, also usable in Flow Changed parameters need a tool update; no-argument constructors from API 66.0
@AuraEnabled Nothing new, if a controller method already does the job Salesforce's mapping; inspect it in Setup The method is small, well named and enforces user access Written for a UI caller, not a model
@RestResource (Apex REST) Nothing new, if an endpoint already exists Salesforce's mapping; inspect it in Setup An integration already depends on it HTTP-shaped contracts are harder for a model
Autolaunched Flow A Flow with input and output variables The Flow's input and output variables An admin owns the rule and changes it often Only autolaunched flows qualify; long runs report no progress

This series builds with invocable Apex because Salesforce documents it most completely for hosted MCP, down to a deployable metadata type.

Build with invocable Apex

The capstone has two tools: AccountHealthTool reads and CreateFollowUpTaskTool writes.

The method signature is the contract

This is the entry point of AccountHealthTool.cls (lines 13 and 21–25):

global with sharing class AccountHealthTool {
    // ...
    @InvocableMethod(
        label='Get Account Health'
        description='Returns a health summary for an Account: open opportunities, open and probability-weighted pipeline, deals closing in the next 30 days, open and high-priority cases, days since the last activity, a 0-100 health score, a status (Healthy, Watch, At Risk) and a one-line summary. Provide accountId, or accountName when the Id is unknown (exact names win, otherwise a partial match is used).'
    )
    global static List<AccountHealthResult> getAccountHealth(List<AccountHealthRequest> requests) {

Four decisions are packed into those lines.

  1. global everywhere. "Only Apex classes with a global method annotated @InvocableMethod can be exposed as MCP tools through this backing type." Salesforce's sample repository also makes the request and result types global, "required for MCP tool discovery".
  2. with sharing, plus user mode. "Apex Actions run as the authenticated user — governor limits and sharing rules apply." Every query also uses WITH USER_MODE, as Salesforce's sample does, so field-level and object-level security apply too.
  3. Lists in, lists out. The same action can run from a Flow over many records, so the class queries each object once for all requests.
  4. A description written for the model. It lists what comes back, names the statuses and explains how the account is found. The label is for humans in Setup; the description is the prompt.

Request and result classes become the schema

When a client calls tools/list, "the agent sees exactly four things per tool: a name, a description, an inputSchema, and an outputSchema." The input class "becomes inputSchema (wrapped in an inputs[] array)"; the output class "becomes outputSchema (nested inside a response envelope as outputValues)". In the org test, Setup showed that envelope as a content array (Step 4). Every @InvocableVariable description ends up in front of the model.

    global class AccountHealthRequest {
        @InvocableVariable(label='Account Id' description='15- or 18-character Id of the Account. Takes precedence over accountName.')
        global String accountId;

        @InvocableVariable(label='Account Name' description='Account name to look up when the Id is unknown. Exact names win; otherwise the most recently modified partial match is used.')
        global String accountName;

        global AccountHealthRequest() {
        }
    }

    global class AccountHealthResult {
        @InvocableVariable(label='Found' description='True when an Account the user can access matched the request.')
        global Boolean found;
        // ... matchType, message, the account, pipeline, cases and activity fields ...
        @InvocableVariable(label='Health Status' description='Healthy (75+), Watch (50-74) or At Risk (below 50).')
        global String healthStatus;
        // ...
        global AccountHealthResult() {
        }
    }

Notice the empty constructors. A release update says: "Starting in API version 66.0, Apex classes used for invocable action parameters must have a visible no-argument constructor." The capstone targets API version 67.0 and declares them global; public would do for an unpackaged class.

Failures come back as data

A tool that throws gives the model nothing useful to say, so AccountHealthTool returns its failures in the result (lines 43–50):

        } catch (QueryException e) {
            // WITH USER_MODE throws when the caller lacks object or field access.
            results = failAll(requests.size(), 'Salesforce denied access to the data this tool reads: ' + e.getMessage());
        } catch (Exception e) {
            // Return the problem to the agent instead of failing the whole MCP tool call.
            results = failAll(requests.size(), 'Account health could not be calculated: ' + e.getMessage());
        }
        return results;

When Neha lacks access, Claude gets found: false and a sentence it can repeat to her.

A write tool that is safe to retry

CreateFollowUpTaskTool validates inputs, checks that the user can see the target record, and refuses duplicates (lines 55–78):

            for (Integer i = 0; i < candidates.size(); i++) {
                Task candidate = candidates[i];
                if (candidate == null) {
                    continue;
                }
                CreateFollowUpTaskResult result = results[i];
                Id target = targetId(candidate);
                if (!visibleTargets.contains(target)) {
                    result.validationErrors.add('Record ' + target + ' was not found or you do not have access to it.');
                    continue;
                }
                Id existing = openDuplicates.get(duplicateKey(candidate));
                if (existing != null) {
                    result.success = true;
                    result.taskId = existing;
                    result.message = 'An open task with this subject and due date already exists on the record; no duplicate was created.';
                    continue;
                }
                toInsert.add(candidate);
                insertPositions.add(i);
            }

            if (!toInsert.isEmpty()) {
                List<Database.SaveResult> saveResults = Database.insert(toInsert, false, AccessLevel.USER_MODE);

The visibility check runs Database.queryWithBinds in user mode, with the object name taken from the schema, never from the model's input. The duplicate check makes the tool idempotent: agents retry, and Acme still ends up with one task. The insert runs in user mode, so a user who can't create tasks gets an error, not a task.

Tests that prove the tool respects the caller

Salesforce's security blog recommends running "Apex tests and Flow tests on your MCP tools with the runAs method". Each capstone test class ends with one (AccountHealthToolTest.cls, lines 170–186):

    @IsTest
    static void respectsTheCallersAccess() {
        User restricted = TestUsers.minimumAccessUser();
        if (restricted == null) {
            return; // Org has no Minimum Access profile; the other tests still cover the logic.
        }

        AccountHealthTool.AccountHealthResult result;
        Test.startTest();
        System.runAs(restricted) {
            result = runOne(byName(HEALTHY_NAME));
        }
        Test.stopTest();

        Assert.isFalse(result.found, 'A user without Account access sees nothing');
        Assert.isNotNull(result.message, 'Explains the failure');
    }

A tool that passes as an administrator proves little; this one proves a minimal-access user gets an explanation and no data.

The declarative twin: an autolaunched Flow

If an admin owns the logic and changes it every quarter, a Flow is the better home. "Only autolaunched flows (not screen flows or scheduled flows) can be exposed as MCP tools. The flow must have defined input and output variables."

The capstone ships Create_Follow_Up_Task_Flow, a declarative version of the task tool. Its variables become the tool's schema:

    <processType>AutoLaunchedFlow</processType>
    <!-- ... -->
    <variables>
        <description>Id of the Account, Opportunity or Case the task follows up on.</description>
        <name>recordId</name>
        <dataType>String</dataType>
        <isCollection>false</isCollection>
        <isInput>true</isInput>
        <isOutput>false</isOutput>
    </variables>
    <!-- ... subject, dueDate, priority and description are inputs too ... -->
    <variables>
        <description>Why the task was not created, when it was not.</description>
        <name>errorMessage</name>
        <dataType>String</dataType>
        <isCollection>false</isCollection>
        <isInput>false</isInput>
        <isOutput>true</isOutput>
    </variables>

A formula rejects a blank record Id or subject and a past due date, and a fault connector copies errors into errorMessage, so the Flow also reports failure as data. The two tools still differ:

Apex tool Flow tool
Targets Account, Opportunity, Case, Contact, Lead Account, Opportunity or Case
Explicit visibility check Yes, in user mode No; relies on the create running as the user
Duplicate protection Returns the existing open task None: a retry creates a second task
Who maintains it A developer, with Apex tests An admin, in Flow Builder

Publish both on one server and the model has to guess between two task tools, so pick one. The capstone publishes the Apex version; the Flow is an optional exercise.

Hands-on: publish the tools on a custom hosted MCP server

You need the Developer Edition org, the Claude External Client App from Day 5, Claude Code from Day 6 and a current Salesforce CLI.

Step 1: deploy the Apex, the Flow and the permission set

cd salesforce
sf update
sf project deploy start \
  --source-dir force-app/main/default/classes \
  --source-dir force-app/main/default/flows \
  --source-dir force-app/main/default/permissionsets \
  --test-level RunSpecifiedTests \
  --tests AccountHealthToolTest --tests CreateFollowUpTaskToolTest \
  --target-org headless360

sf org assign permset --name Headless_Assistant_User --target-org headless360
sf apex run --file scripts/apex/seed-demo-data.apex --target-org headless360

sf apex run test --class-names AccountHealthToolTest --class-names CreateFollowUpTaskToolTest \
  --code-coverage --result-format human --wait 10 --target-org headless360

Headless_Assistant_User grants access to the two classes and the Flow, API Enabled, Edit Tasks, and read on Account, Opportunity and Case. The seed script gives Acme a large deal closing soon, an open high-priority case and 40 days without activity, so it should rate Watch or At Risk.

Observed on 2026-10-03 in a Developer Edition org: every Apex test passed, both respectsTheCallersAccess tests ran rather than returning early, and coverage was 98% for AccountHealthTool and 96% for CreateFollowUpTaskTool.

Step 2 (route A): deploy the server as metadata

A custom server is metadata of type McpServerDefinition: "The metadata can be source controlled and CI/CD deployed alongside the Apex class." The capstone's definition names the two tools:

<McpServerDefinition xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>Business tools for the Headless 360 Assistant: an Account health summary and validated follow-up task creation, both written in Apex and run as the signed-in user.</description>
    <masterLabel>Headless Assistant Tools</masterLabel>
    <tools>
        <apiDefinition>
            <apiIdentifier>aa:apex-AccountHealthTool</apiIdentifier>
            <apiSource>API_CATALOG</apiSource>
            <operation>AccountHealthTool</operation>
        </apiDefinition>
        <descriptionOverride>Returns a health summary for one Account: open opportunities, open and weighted pipeline, deals closing in 30 days, open and high-priority cases, days since last activity, a 0-100 score, a Healthy/Watch/At Risk status and a one-line summary. Pass accountId, or accountName (partial names match).</descriptionOverride>
        <toolName>getAccountHealth</toolName>
        <toolTitle>Get Account Health</toolTitle>
    </tools>
    <!-- createFollowUpTask follows the same pattern -->
</McpServerDefinition>

toolName becomes the MCP name; descriptionOverride becomes the description and "Falls back to @InvocableMethod description if empty"; apiDefinition produces both schemas; toolTitle is "Not used for tool selection". Deploy it:

sf project deploy start --source-dir force-app/main/default/mcpServerDefinitions --target-org headless360

The aa:apex-<ClassName> format is copied from Salesforce's sample repository, and it worked: observed on 2026-10-03 in a Developer Edition org, this deploy succeeded and the Setup fallback wasn't needed. One thing did fail first. The definition was originally named Headless_Assistant_Tools, and that name failed metadata validation on deploy, so the capstone's file is now HeadlessAssistantTools.mcpServerDefinition-meta.xml and the server name is HeadlessAssistantTools. If the CLI says the suffix is unknown, run sf update.

Prefer this route, and keep the definition as the source of truth

Only a metadata deploy keeps your toolNames. Route B below works, but tools added there get names Salesforce generates.

Step 3 (route B): create the server in Setup

  1. Setup → Quick Find "API Catalog" → MCP Servers → Create Salesforce MCP Server.
  2. Enter the label Headless Assistant Tools, the name HeadlessAssistantTools and a description, then click Create. "After you create the server, you can edit the label and description, but not the name."
  3. Open the server from the Salesforce Servers tab, click Add Server Assets, then Add Tools. Choose the Apex actions list view (label may differ in your org), switch Get Account Health and Create Follow-Up Task from Add Tool to Added, and click Save.
  4. Note the tool names Salesforce generates. Observed on 2026-10-04: a tool added in Setup is named <Name><type>_<Name>, so the task action became CreateFollowUpTaskToolapex_CreateFollowUpTaskTool, with the Apex action's own description, not createFollowUpTask. Any client that lists tools by name must use the generated names.
  5. On the server's Tools tab, set annotations (Help's examples: "Read-only, Destructive, Idempotent, and Open-world"). Mark the health tool Read-only and the task tool Idempotent.

Two rules apply to every edit: "Make sure to deactivate the MCP server if it's active", and "Updating the underlying Apex class (adding parameters, changing types) requires updating the tool configuration in Setup to stay in sync." To add read tools, Add Server Assets → Add from Server reuses standard tools, but "only tools based on OAS operations are available to add".

Step 4: activate the server and copy its URL

Open Headless Assistant Tools on the Salesforce Servers tab, click Activate, and copy the Server URL. Sandboxes and scratch orgs use /sandbox/custom/ instead of /custom/. The org test showed this form, with no /d/<mydomain>/ variant:

https://api.salesforce.com/platform/mcp/v1/custom/HeadlessAssistantTools

The server page shows Type Custom, Server Status Active, Tools 2 and Prompts 0. Read its Tools tab: it shows what the model will see. After the metadata deploy it listed Get Account Health (getAccountHealth): "Returns a health summary for one Account: open opportunities, open and weighted pipeline, deals closing in 30 days, open and high-priority cases, days since last activity, a 0-100 score, a Healthy/Watch/At Risk status and a one-line summary. Pass accountId, or accountName (partial names match)." And Create Follow-Up Task (createFollowUpTask): "Creates an open follow-up Task for the current user on an Account, Opportunity or Case (recordId), or a Contact or Lead. Requires recordId and subject; dueDate (YYYY-MM-DD, default today + 3) and priority (High, Normal, Low) are optional. Returns the existing open task instead of a duplicate. This tool changes data."

The Headless Assistant Tools custom server: Active, 2 tools from Apex actions with their descriptions and generated schemas

Both show Source "Apex actions", an Input Schema of { inputs: array of object } and an Output Schema of { content: array of object }. That is the invocable wrapper, not your fields: the request and result classes sit inside those arrays. It is why the descriptions name every argument, and why a client calls createFollowUpTask with {"inputs": [{"recordId": "…", "subject": "…"}]}.

The same External Client App works here: "You cannot restrict access to a specific MCP server through the ECA configuration, but you control access to the tools that compose those servers." The permission set on the Apex classes is that control.

Optional: the Flow, and what Setup edits do to tool names

To try the Flow, deactivate the server, choose Add Server Assets → Add Tools, add Create Follow-Up Task Flow from the flows (labels may differ), save and reactivate. Observed on 2026-10-04, the server then listed three tools: getAccountHealth, unchanged; the re-added Apex action as CreateFollowUpTaskTool (CreateFollowUpTaskToolapex_CreateFollowUpTaskTool); and Create_Follow_Up_Task_Flow (Create_Follow_Up_Task_Flowflow_Create_Follow_Up_Task_Flow), Source "Flows", described as "Declarative equivalent of the CreateFollowUpTaskTool Apex action: creates an open follow-up Task on an Account, Opportunity or Case (WhatId). Outputs taskId, or errorMessage when validation or the create fails. Can be added to the custom hosted MCP server as a Flow-backed tool." Claude Code's /mcp showed the same three ids. The name createFollowUpTask was gone.

Redeploying the definition afterwards succeeded but reported the component Unchanged: the Metadata API may not overwrite tools changed in Setup. So treat the deployed definition as the source of truth, don't edit its tools in Setup, and never let a client rely on a tool name staying put. The capstone's app fails closed: every tool on a server starts disabled, only the names in SF_MCP_CUSTOM_READ_TOOLS (default getAccountHealth) are enabled, and only a name in SF_MCP_CUSTOM_WRITE_TOOLS (default createFollowUpTask) can be proposed and approved. A renamed tool such as CreateFollowUpTaskToolapex_CreateFollowUpTaskTool is in neither list, so it stays switched off until you add it to one. Remove one of the two task tools when you are done, so the model isn't left choosing between them.

The same server after editing tools in Setup: 3 tools with generated ids such as CreateFollowUpTaskToolapex_CreateFollowUpTaskTool

Step 5: call your tools from Claude Code

Add the server with the Claude app's consumer key, as on Day 6:

claude mcp add --transport http salesforce-headless-assistant-tools "<CUSTOM_SERVER_URL>" \
  --callback-port 38000 --client-id "<CLAUDE_APP_CONSUMER_KEY>"
claude    # then type /mcp, pick salesforce-headless-assistant-tools and choose Authenticate

Then ask:

  1. How healthy is the Acme Global Tech account? Claude should call getAccountHealth and report a score, a status and the summary. Observed on 2026-10-04 with the seeded data: Watch, 55/100; 2 open opportunities worth $215,000 ($174,750 weighted), 1 closing within 30 days; 2 open cases, 1 high priority; last activity 42 days earlier.
  2. Create a follow-up task on Acme Global Tech to send the renewal quote, due in 5 days. Approve the call, then ask again. The second call should return the same Task Id, with the message "An open task with this subject and due date already exists on the record; no duplicate was created." In the org test the repeat returned the same Id, and Acme's Activity showed a single "Send renewal quote".

If Claude reaches for soqlQuery from another server instead, the descriptions overlap or too many tools are connected.

Claude Code calling the custom server: Acme Global Tech is on Watch with a health score of 55/100

Let a coding agent help

Salesforce distinguishes MCP tools for coding agents from tools for business agents. You just built business-agent tools; coding-agent tools help you build them.

The Salesforce DX MCP Server (@salesforce/mcp) "includes over 60 MCP tools"; Salesforce's Summer '26 guide labels it beta. The README's Claude Code .mcp.json, verbatim:

{
  "mcpServers": {
    "Salesforce DX": {
      "command": "npx",
      "args": ["-y", "@salesforce/mcp",
               "--orgs", "DEFAULT_TARGET_ORG",
               "--toolsets", "orgs,metadata,data,users",
               "--tools", "run_apex_test",
               "--allow-non-ga-tools"]
    }
  }
}

Each flag is a governance decision. --orgs is the required allowlist (DEFAULT_TARGET_ORG, ALLOW_ALL_ORGS, or a username or alias); use headless360 so the agent reaches your Developer Edition org and nothing else. --toolsets picks groups such as metadata; enabling all of them "can overwhelm the LLM context." --tools run_apex_test adds one tool without its toolset, and --allow-non-ga-tools enables tools not marked GA.

Tools "pass usernames around instead of tokens". Unlike hosted servers, it runs on "Your local machine" with "CLI credentials", so its reach is your CLI login's, narrowed only by the orgs and tools you allow. Share the .mcp.json with your team rather than an untested claude mcp add command.

Two more pieces round out the kit: Salesforce's skills library (npx skills add forcedotcom/sf-skills, 240 entries on September 30; "Expect frequent changes"), and the Claude Code plugin (/plugin install salesforce-development@claude-plugins-official), which bundles about 40 skills and the salesforce-api-context, salesforce-metadata-experts and salesforce-lsp servers.

A realistic loop: ask Claude Code to "add a daysSinceLastCase field to AccountHealthResult, update the test, run both test classes against headless360, and show me the diff before deploying." Review the diff, deploy, then check the server's Tools tab: a changed Apex class needs the tool configuration updated, and an update made in Setup can rename the tool.

What can go wrong

Symptom Likely cause Fix
Your Apex action isn't offered in Add Tools The class, the method or its types aren't global Make them global; only a global @InvocableMethod qualifies
Deploy or run-time errors on a request or result class after moving to API 66.0 or later No visible no-argument constructor Add a public (or, in a package, global) no-argument constructor
Claude still sends the old parameters after you changed the Apex The published tool configuration is out of sync Deactivate the server, update the tool in Setup, reactivate
Claude calls soqlQuery instead of getAccountHealth Overlapping descriptions, or too many tools connected Sharpen the description; curate a smaller custom server
The tool works for Arjun but returns "Salesforce denied access…" for Neha Tested as an admin; permission set not assigned Assign Headless_Assistant_User; keep the runAs tests
Your Flow isn't in the Flows list It is a screen or scheduled flow, or it has no input and output variables Rebuild it as an autolaunched Flow with defined variables
Tools named like CreateFollowUpTaskToolapex_CreateFollowUpTaskTool The tool was added or re-added in Setup, which generates names Keep the deployed definition as the source; list the generated names in your client's read or write lists
The definition deploy fails on the server's name A name such as Headless_Assistant_Tools Use HeadlessAssistantTools, as the capstone does

What runs as whom

Every custom tool runs as the user who signed in, Apex and Flow alike. Your code can still widen access by accident. A class declared without sharing, or a query without user mode, can return data the user could not see in Lightning, and the agent will happily pass it on. Keep with sharing and WITH USER_MODE in every tool, and grant class access through a permission set. Annotations such as Read-only are hints to the client, not enforcement; the confirmation step for createFollowUpTask still belongs in the client, as on Day 9.

Today's checklist

  • I can explain when a custom tool beats a generic sobject-all call.
  • My invocable classes, methods and request/result types are global, with no-argument constructors.
  • Every @InvocableVariable has a description written for the model.
  • Both Apex test classes pass, including the runAs tests.
  • HeadlessAssistantTools exists (by metadata, ideally), its Tools tab shows getAccountHealth and createFollowUpTask, and it is active.
  • Claude Code called getAccountHealth, and a repeated task request returned the existing task.
  • My DX MCP Server entry allowlists only my Developer Edition org.

Frequently asked questions

Do I need Apex to build a custom MCP tool?

No. Custom servers also publish autolaunched Flows, Apex REST endpoints, @AuraEnabled methods, Named Query API queries, Prompt Builder prompts, Agentforce agents and API Catalog endpoints.

Can I deploy a custom MCP server with the Salesforce CLI?

Yes. Salesforce documents the McpServerDefinition type and says custom servers can "Be deployed via Metadata API between sandboxes and production". The capstone's definition deployed with sf project deploy start in a Developer Edition org on 2026-10-03, after renaming it to HeadlessAssistantTools. It is also the only route that keeps your tool names; tools added in Setup get generated ones.

Can a managed package ship an MCP server?

Not yet: ISV managed packages "cannot yet include MCP server configurations directly". They can include invocable, @AuraEnabled and @RestResource Apex and autolaunched Flows for customers to publish on their own servers.

What's next

You now have tools that carry Acme's rules and a coding agent to maintain them. On Day 13 we design the whole system: when one assistant is enough, when to add a planner or several agents, where Agentforce fits next to external agents, what the beta Headless 360 MCP Server changes, and whether Acme's next interface should be native React on Salesforce or an external Next.js app. You will finish with an architecture decision record you can defend.

Sources

Verified against the sources below on September 30, 2026. Salesforce ships Headless 360 changes often: check the linked docs if a screen looks different.

  1. Custom MCP Servers — Salesforce Hosted MCP Servers guide
  2. Apex Actions (custom tools)
  3. Flows (custom tools)
  4. Create Custom Salesforce MCP Servers in API Catalog
  5. Enforcing No-Argument Constructor on Apex Classes Used for Invocable Action Parameters — release update, API version 66.0
  6. Expose Custom Apex as a Hosted MCP Tool for Agents
  7. Salesforce DX MCP Server (README)
  8. Headless Development with Skills and a Claude Code Plugin
Day 12 · Cheat sheet

Everything from today on one page. Tap to zoom, or download it for later.

Download PNG
Day 12 cheat sheet: Custom MCP Tools + Apex/Flow Business LogicOpen PNG
Day 12 cheat sheet: Custom MCP Tools + Apex/Flow Business Logic
Day 12 cheat sheet: Custom MCP Tools + Apex/Flow Business Logic
All 15 days in this series
Share
Discussion

Comments

Loading comments...

Add a comment

Comments are reviewed before they appear.