@salesforce/mcp 0.6.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -158,6 +158,7 @@ These are the available toolsets:
158
158
  - `data` - [Tools to manage the data in your org, such as listing all accounts.](README.md#data-toolset)
159
159
  - `users` - [Tools to manage org users, such as assigning a permission set.](README.md#users-toolset)
160
160
  - `metadata` - [Tools to deploy and retrieve metadata to and from your org and your DX project.](README.md#metadata-toolset)
161
+ - `testing` - [Tools to test your code and features](README.md#testing-toolset)
161
162
 
162
163
  This example shows how to enable the `data`, `orgs`, and `metadata` toolsets when configuring the MCP server for VS Code:
163
164
 
@@ -203,6 +204,14 @@ Includes these tools:
203
204
 
204
205
  - `sf-deploy-metadata` - Deploys metadata from your DX project to an org.
205
206
  - `sf-retrieve-metadata` - Retrieves metadata from your org to your DX project.
207
+ -
208
+
209
+ #### Testing Toolset
210
+
211
+ Includes these tools:
212
+
213
+ - `sf-test-agents` - Executes agent tests in your org.
214
+ - `sf-test-apex` - Executes apex tests in your org
206
215
 
207
216
  ## Configure Other Clients to Use the Salesforce DX MCP Server
208
217
 
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export default class McpServerCommand extends Command {
4
4
  static description: string;
5
5
  static flags: {
6
6
  orgs: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
7
- toolsets: import("@oclif/core/interfaces").OptionFlag<("data" | "metadata" | "all" | "users" | "orgs")[], import("@oclif/core/interfaces").CustomOptions>;
7
+ toolsets: import("@oclif/core/interfaces").OptionFlag<("data" | "metadata" | "all" | "users" | "orgs" | "testing")[], import("@oclif/core/interfaces").CustomOptions>;
8
8
  version: import("@oclif/core/interfaces").BooleanFlag<void>;
9
9
  'no-telemetry': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
package/lib/index.js CHANGED
@@ -20,11 +20,12 @@ import * as core from './tools/core/index.js';
20
20
  import * as orgs from './tools/orgs/index.js';
21
21
  import * as data from './tools/data/index.js';
22
22
  import * as users from './tools/users/index.js';
23
+ import * as testing from './tools/testing/index.js';
23
24
  import * as metadata from './tools/metadata/index.js';
24
25
  import Cache from './shared/cache.js';
25
26
  import { Telemetry } from './telemetry.js';
26
27
  import { SfMcpServer } from './sf-mcp-server.js';
27
- const TOOLSETS = ['all', 'orgs', 'data', 'users', 'metadata'];
28
+ const TOOLSETS = ['all', 'testing', 'orgs', 'data', 'users', 'metadata'];
28
29
  /**
29
30
  * Sanitizes an array of org usernames by replacing specific orgs with a placeholder.
30
31
  * Special values (DEFAULT_TARGET_ORG, DEFAULT_TARGET_DEV_HUB, ALLOW_ALL_ORGS) are preserved.
@@ -128,8 +129,6 @@ You can also use special values to control access to orgs:
128
129
  tools: {},
129
130
  },
130
131
  }, { telemetry: this.telemetry });
131
- // // TODO: Should we add annotations to our tools? https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
132
- // // TODO: Move tool names into a shared file, that way if we reference them in multiple places, we can update them in one place
133
132
  const enabledToolsets = new Set(flags.toolsets);
134
133
  const all = enabledToolsets.has('all');
135
134
  // ************************
@@ -164,6 +163,14 @@ You can also use special values to control access to orgs:
164
163
  users.registerToolAssignPermissionSet(server);
165
164
  }
166
165
  // ************************
166
+ // testing TOOLS
167
+ // ************************
168
+ if (all || enabledToolsets.has('testing')) {
169
+ this.logToStderr('Registering testing tools');
170
+ testing.registerToolRunApexTest(server);
171
+ testing.registerToolRunAgentTest(server);
172
+ }
173
+ // ************************
167
174
  // METADATA TOOLS
168
175
  // ************************
169
176
  if (all || enabledToolsets.has('metadata')) {
@@ -69,7 +69,11 @@ This tool handles three distinct scenarios:
69
69
  EXAMPLE USAGE:
70
70
  - When user says "Do X for my org" → defaultTargetOrg=false, defaultDevHub=false
71
71
  - When user says "For my default org" → defaultTargetOrg=true
72
- - When user says "For my default dev hub" → defaultDevHub=true`, getUsernameParamsSchema.shape, async ({ defaultTargetOrg, defaultDevHub, directory }) => {
72
+ - When user says "For my default dev hub" → defaultDevHub=true`, getUsernameParamsSchema.shape, {
73
+ title: 'Get Username',
74
+ readOnlyHint: true,
75
+ openWorldHint: false,
76
+ }, async ({ defaultTargetOrg, defaultDevHub, directory }) => {
73
77
  try {
74
78
  process.chdir(directory);
75
79
  const generateResponse = (defaultFromConfig) => textResponse(`ALWAYS notify the user the following 3 (maybe 4) pieces of information:
@@ -63,7 +63,10 @@ Resume the deployment to my org
63
63
  Resume scratch org creation
64
64
  Resume job 2SR1234567890
65
65
  Resume agent tests
66
- `, resumeParamsSchema.shape, async ({ jobId, wait, usernameOrAlias, directory }) => {
66
+ `, resumeParamsSchema.shape, {
67
+ title: 'Resume',
68
+ openWorldHint: false,
69
+ }, async ({ jobId, wait, usernameOrAlias, directory }) => {
67
70
  if (!jobId) {
68
71
  return textResponse('The jobId parameter is required.', true);
69
72
  }
@@ -35,7 +35,11 @@ export const queryOrgParamsSchema = z.object({
35
35
  directory: directoryParam,
36
36
  });
37
37
  export const registerToolQueryOrg = (server) => {
38
- server.tool('sf-query-org', 'Run a SOQL query against a Salesforce org.', queryOrgParamsSchema.shape, async ({ query, usernameOrAlias, directory }) => {
38
+ server.tool('sf-query-org', 'Run a SOQL query against a Salesforce org.', queryOrgParamsSchema.shape, {
39
+ title: 'Query Org',
40
+ openWorldHint: false,
41
+ readOnlyHint: true,
42
+ }, async ({ query, usernameOrAlias, directory }) => {
39
43
  try {
40
44
  if (!usernameOrAlias)
41
45
  return textResponse('The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool', true);
@@ -82,7 +82,11 @@ Deploy this file to my org
82
82
  Deploy the manifest
83
83
  Deploy X metadata to my org
84
84
  Deploy X to my org and run A,B and C apex tests.
85
- `, deployMetadataParams.shape, async ({ sourceDir, usernameOrAlias, apexTests, apexTestLevel, directory, manifest }) => {
85
+ `, deployMetadataParams.shape, {
86
+ title: 'Deploy Metadata',
87
+ destructiveHint: true,
88
+ openWorldHint: false,
89
+ }, async ({ sourceDir, usernameOrAlias, apexTests, apexTestLevel, directory, manifest }) => {
86
90
  if (apexTests && apexTestLevel) {
87
91
  return textResponse("You can't specify both `apexTests` and `apexTestLevel` parameters.", true);
88
92
  }
@@ -55,7 +55,11 @@ Retrieve changes from my org
55
55
  Retrieve this file from my org
56
56
  Retrieve the metadata in the manifest
57
57
  Retrieve X metadata from my org
58
- `, retrieveMetadataParams.shape, async ({ sourceDir, usernameOrAlias, directory, manifest }) => {
58
+ `, retrieveMetadataParams.shape, {
59
+ title: 'Retrieve Metadata',
60
+ openWorldHint: false,
61
+ destructiveHint: true,
62
+ }, async ({ sourceDir, usernameOrAlias, directory, manifest }) => {
59
63
  if (sourceDir && manifest) {
60
64
  return textResponse("You can't specify both `sourceDir` and `manifest` parameters.", true);
61
65
  }
@@ -41,7 +41,11 @@ Example usage:
41
41
  Can you list all Salesforce orgs for me
42
42
  List all Salesforce orgs
43
43
  List all orgs
44
- `, listAllOrgsParamsSchema.shape, async ({ directory }) => {
44
+ `, listAllOrgsParamsSchema.shape, {
45
+ title: 'List All Orgs',
46
+ readOnlyHint: true,
47
+ openWorldHint: false,
48
+ }, async ({ directory }) => {
45
49
  try {
46
50
  process.chdir(directory);
47
51
  const orgs = await getAllAllowedOrgs();
@@ -0,0 +1,2 @@
1
+ export * from './sf-test-apex.js';
2
+ export * from './sf-test-agents.js';
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Copyright 2025, Salesforce, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ export * from './sf-test-apex.js';
17
+ export * from './sf-test-agents.js';
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { SfMcpServer } from '../../sf-mcp-server.js';
3
+ declare const runAgentTestsParam: z.ZodObject<{
4
+ agentApiName: z.ZodString;
5
+ usernameOrAlias: z.ZodString;
6
+ directory: z.ZodEffects<z.ZodString, string, string>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ directory: string;
9
+ usernameOrAlias: string;
10
+ agentApiName: string;
11
+ }, {
12
+ directory: string;
13
+ usernameOrAlias: string;
14
+ agentApiName: string;
15
+ }>;
16
+ export type AgentRunTests = z.infer<typeof runAgentTestsParam>;
17
+ export declare const registerToolRunAgentTest: (server: SfMcpServer) => void;
18
+ export {};
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Copyright 2025, Salesforce, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { z } from 'zod';
17
+ import { AgentTester } from '@salesforce/agents';
18
+ import { Duration } from '@salesforce/kit';
19
+ import { directoryParam, usernameOrAliasParam } from '../../shared/params.js';
20
+ import { textResponse } from '../../shared/utils.js';
21
+ import { getConnection } from '../../shared/auth.js';
22
+ const runAgentTestsParam = z.object({
23
+ agentApiName: z.string().describe(`Agent test to run
24
+ if unsure, list all files matching the pattern *.aiEvaluationDefinition-meta.xml
25
+ only one test can be executed at a time
26
+ `),
27
+ usernameOrAlias: usernameOrAliasParam,
28
+ directory: directoryParam,
29
+ });
30
+ /*
31
+ * Run Agent tests in a Salesforce org.
32
+ *
33
+ * Parameters:
34
+ * - agentApiName: this will be the aiEvaluationDefinition's name
35
+ * - usernameOrAlias: Username or alias of the Salesforce org to run tests in.
36
+ * - directory: Directory of the local project.
37
+ *
38
+ * Returns:
39
+ * - textResponse: Test result.
40
+ */
41
+ export const registerToolRunAgentTest = (server) => {
42
+ server.tool('sf-test-agents', `Run Agent tests in an org.
43
+
44
+ AGENT INSTRUCTIONS:
45
+ If the user doesn't specify what to test, take context from the currently open file
46
+ This will ONLY run Agent tests, NOT apex tests, lightning tests, flow tests, or any other type of test.
47
+
48
+ this should be chosen when a file in the 'aiEvaluationDefinitions' directory is mentioned
49
+
50
+ EXAMPLE USAGE:
51
+ Run tests for the X agent
52
+ Run this test
53
+ `, runAgentTestsParam.shape, {
54
+ title: 'Run Agent Tests',
55
+ openWorldHint: false,
56
+ }, async ({ usernameOrAlias, agentApiName, directory }) => {
57
+ if (!usernameOrAlias)
58
+ return textResponse('The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool', true);
59
+ // needed for org allowlist to work
60
+ process.chdir(directory);
61
+ const connection = await getConnection(usernameOrAlias);
62
+ try {
63
+ const agentTester = new AgentTester(connection);
64
+ const test = await agentTester.start(agentApiName);
65
+ const result = await agentTester.poll(test.runId, { timeout: Duration.minutes(10) });
66
+ return textResponse(`Test result: ${JSON.stringify(result)}`);
67
+ }
68
+ catch (e) {
69
+ return textResponse(`Failed to run Agent Tests: ${e instanceof Error ? e.message : 'Unknown error'}`, true);
70
+ }
71
+ });
72
+ };
73
+ //# sourceMappingURL=sf-test-agents.js.map
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import { TestLevel } from '@salesforce/apex-node';
3
+ import { SfMcpServer } from '../../sf-mcp-server.js';
4
+ declare const runApexTestsParam: z.ZodObject<{
5
+ testLevel: z.ZodEnum<[TestLevel.RunLocalTests, TestLevel.RunAllTestsInOrg, TestLevel.RunSpecifiedTests]>;
6
+ classNames: z.ZodArray<z.ZodString, "many">;
7
+ usernameOrAlias: z.ZodString;
8
+ directory: z.ZodEffects<z.ZodString, string, string>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ testLevel: TestLevel;
11
+ directory: string;
12
+ usernameOrAlias: string;
13
+ classNames: string[];
14
+ }, {
15
+ testLevel: TestLevel;
16
+ directory: string;
17
+ usernameOrAlias: string;
18
+ classNames: string[];
19
+ }>;
20
+ export type ApexRunTests = z.infer<typeof runApexTestsParam>;
21
+ export declare const registerToolRunApexTest: (server: SfMcpServer) => void;
22
+ export {};
@@ -0,0 +1,86 @@
1
+ /*
2
+ * Copyright 2025, Salesforce, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { z } from 'zod';
17
+ import { TestService } from '@salesforce/apex-node';
18
+ import { directoryParam, usernameOrAliasParam } from '../../shared/params.js';
19
+ import { textResponse } from '../../shared/utils.js';
20
+ import { getConnection } from '../../shared/auth.js';
21
+ const runApexTestsParam = z.object({
22
+ testLevel: z.enum(["RunLocalTests" /* TestLevel.RunLocalTests */, "RunAllTestsInOrg" /* TestLevel.RunAllTestsInOrg */, "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */]).describe(`Apex test level
23
+
24
+ AGENT INSTRUCTIONS
25
+ Choose the correct value based on what tests are meant to be executed in some of these ways:
26
+
27
+ RunLocalTests="Run all tests in the org, except the ones that originate from installed managed and unlocked packages."
28
+ RunAllTestsInOrg="Run all tests in the org, including tests of managed packages"
29
+ RunSpecifiedTests="Run the Apex tests I specify, these will be specified in the classNames parameter"
30
+ `),
31
+ classNames: z.array(z.string()).describe(`Apex tests classes to run.
32
+ if Running all tests, all tests should be listed
33
+ if unsure, find apex classes matching the pattern **/classes/*.cls, that include the @isTest decorator in the file and then join their test names together with ','
34
+ `),
35
+ usernameOrAlias: usernameOrAliasParam,
36
+ directory: directoryParam,
37
+ });
38
+ /*
39
+ * Run Apex tests in a Salesforce org.
40
+ *
41
+ * Parameters:
42
+ * - testLevel: 'RunSpecifiedTests', 'RunLocalTests', 'RunAllTestsInOrg', used to specify the specific test-level.
43
+ * - classNames: if testLevel='RunSpecifiedTests', this will be the specified tests to run
44
+ * - usernameOrAlias: Username or alias of the Salesforce org to run tests in.
45
+ * - directory: Directory of the local project.
46
+ *
47
+ * Returns:
48
+ * - textResponse: Test result.
49
+ */
50
+ export const registerToolRunApexTest = (server) => {
51
+ server.tool('sf-test-apex', `Run Apex tests in an org.
52
+
53
+ AGENT INSTRUCTIONS:
54
+ If the user doesn't specify what to test, take context from the currently open file
55
+ This will ONLY run APEX tests, NOT agent tests, lightning tests, flow tests, or any other type of test.
56
+
57
+ this should be chosen when a file in the 'classes' directory is mentioned
58
+
59
+ EXAMPLE USAGE:
60
+ Run tests A, B, C.
61
+ Run the tests, find apex classes matching the pattern **/classes/*.cls, that include the @isTest decorator in the file and then join their test names together with ','
62
+ Run all tests in the org.
63
+ `, runApexTestsParam.shape, {
64
+ title: 'Apex Tests',
65
+ openWorldHint: false,
66
+ }, async ({ testLevel, usernameOrAlias, classNames, directory }) => {
67
+ if (testLevel !== "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */ && classNames?.length && classNames?.length >= 1) {
68
+ return textResponse("You can't specify which tests to run without setting testLevel='RunSpecifiedTests'", true);
69
+ }
70
+ if (!usernameOrAlias)
71
+ return textResponse('The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool', true);
72
+ // needed for org allowlist to work
73
+ process.chdir(directory);
74
+ const connection = await getConnection(usernameOrAlias);
75
+ try {
76
+ const testService = new TestService(connection);
77
+ const payload = await testService.buildAsyncPayload(testLevel, classNames.join(','));
78
+ const result = (await testService.runTestAsynchronous(payload, false));
79
+ return textResponse(`Test result: ${JSON.stringify(result)}`);
80
+ }
81
+ catch (e) {
82
+ return textResponse(`Failed to run Apex Tests: ${e instanceof Error ? e.message : 'Unknown error'}`, true);
83
+ }
84
+ });
85
+ };
86
+ //# sourceMappingURL=sf-test-apex.js.map
@@ -57,7 +57,10 @@ Set the permission set MyPermSet on behalf of my-alias.`),
57
57
  directory: directoryParam,
58
58
  });
59
59
  export const registerToolAssignPermissionSet = (server) => {
60
- server.tool('sf-assign-permission-set', 'Assign a permission set to one or more org users.', assignPermissionSetParamsSchema.shape, async ({ permissionSetName, usernameOrAlias, onBehalfOf, directory }) => {
60
+ server.tool('sf-assign-permission-set', 'Assign a permission set to one or more org users.', assignPermissionSetParamsSchema.shape, {
61
+ title: 'Assign Permission Set',
62
+ openWorldHint: false,
63
+ }, async ({ permissionSetName, usernameOrAlias, onBehalfOf, directory }) => {
61
64
  try {
62
65
  if (!usernameOrAlias)
63
66
  return textResponse('The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool', true);