From 537652d5d842eb14eea47ff68ded4f7a6d4a85ae Mon Sep 17 00:00:00 2001 From: bfmvsa Date: Sun, 20 Jul 2025 20:11:53 +0200 Subject: [PATCH] feat: add apex-run tool --- README.md | 6 ++++ src/index.ts | 10 ++++++ src/shared/tools.ts | 5 ++- src/tools/apex/index.ts | 17 +++++++++ src/tools/apex/sf-apex-run.ts | 67 +++++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/tools/apex/index.ts create mode 100644 src/tools/apex/sf-apex-run.ts diff --git a/README.md b/README.md index 8c2dcc31..cece39da 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/src/index.ts b/src/index.ts index 1285c9a0..1923f44d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -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 // ************************ diff --git a/src/shared/tools.ts b/src/shared/tools.ts index d6f1557f..b70f3ab1 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -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]; @@ -62,6 +62,7 @@ export function determineToolsetsToEnable( metadata: true, orgs: true, testing: true, + apex: true, users: true, }; } @@ -75,6 +76,7 @@ export function determineToolsetsToEnable( metadata: true, orgs: true, testing: true, + apex: true, users: true, }; } @@ -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'), }; } diff --git a/src/tools/apex/index.ts b/src/tools/apex/index.ts new file mode 100644 index 00000000..30d3bd35 --- /dev/null +++ b/src/tools/apex/index.ts @@ -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'; diff --git a/src/tools/apex/sf-apex-run.ts b/src/tools/apex/sf-apex-run.ts new file mode 100644 index 00000000..efba66fe --- /dev/null +++ b/src/tools/apex/sf-apex-run.ts @@ -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; + +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); + } + } + ); +};