Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ Includes these tools:
- `sf-test-agents` - Executes agent tests in your org.
- `sf-test-apex` - Executes apex tests in your org

#### Apex Toolset

Includes this tool:

- `sf-apex-run` - Executes anonymous apex code against a Salesforce org.

## Configure Other Clients to Use the Salesforce DX MCP Server

**Cursor**
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as orgs from './tools/orgs/index.js';
import * as data from './tools/data/index.js';
import * as users from './tools/users/index.js';
import * as testing from './tools/testing/index.js';
import * as apex from './tools/apex/index.js';
import * as metadata from './tools/metadata/index.js';
import * as dynamic from './tools/dynamic/index.js';
import Cache from './shared/cache.js';
Expand Down Expand Up @@ -220,6 +221,15 @@ You can also use special values to control access to orgs:
testing.registerToolTestAgent(server);
}

// ************************
// APEX TOOLS
// ************************
if (toolsetsToEnable.apex) {
this.logToStderr('Registering apex tools');
// assign permission set
apex.registerToolApexRun(server);
}

// ************************
// METADATA TOOLS
// ************************
Expand Down
5 changes: 4 additions & 1 deletion src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ToolInfo } from './types.js';
import Cache from './cache.js';

export const TOOLSETS = ['orgs', 'data', 'users', 'metadata', 'testing', 'experimental'] as const;
export const TOOLSETS = ['orgs', 'data', 'users', 'metadata', 'testing', 'apex', 'experimental'] as const;

type Toolset = (typeof TOOLSETS)[number];

Expand Down Expand Up @@ -62,6 +62,7 @@ export function determineToolsetsToEnable(
metadata: true,
orgs: true,
testing: true,
apex: true,
users: true,
};
}
Expand All @@ -75,6 +76,7 @@ export function determineToolsetsToEnable(
metadata: true,
orgs: true,
testing: true,
apex: true,
users: true,
};
}
Expand All @@ -87,6 +89,7 @@ export function determineToolsetsToEnable(
metadata: toolsets.includes('metadata'),
orgs: toolsets.includes('orgs'),
testing: toolsets.includes('testing'),
apex: toolsets.includes('apex'),
users: toolsets.includes('users'),
};
}
Expand Down
17 changes: 17 additions & 0 deletions src/tools/apex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export * from './sf-apex-run.js';
67 changes: 67 additions & 0 deletions src/tools/apex/sf-apex-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2025, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { z } from 'zod';

import { ExecuteService } from '@salesforce/apex-node';
import { getConnection } from '../../shared/auth.js';
import { textResponse } from '../../shared/utils.js';
import { directoryParam, usernameOrAliasParam } from '../../shared/params.js';
import { SfMcpServer } from '../../sf-mcp-server.js';

export const apexRunParamsSchema = z.object({
apexCode: z.string().describe('Anonymous apex code to execute'),
usernameOrAlias: usernameOrAliasParam,
directory: directoryParam,
});

export type ApexRunOptions = z.infer<typeof apexRunParamsSchema>;

export const registerToolApexRun = (server: SfMcpServer): void => {
server.tool(
'sf-apex-run',
'Run anonymous apex code against a Salesforce org.',
apexRunParamsSchema.shape,
{
title: 'Apex Run',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
},
async ({ apexCode, usernameOrAlias, directory }) => {
try {
if (!usernameOrAlias)
return textResponse(
'The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool',
true
);
process.chdir(directory);

const connection = await getConnection(usernameOrAlias);
const executeService = new ExecuteService(connection);
const result = await executeService.executeAnonymous({
apexCode,
});

return textResponse('Apex Run Result: ' + JSON.stringify(result));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return textResponse(`Failed to query org: ${errorMessage}`, true);
}
}
);
};