@salesforce/mcp 0.7.0 → 0.8.2

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.js CHANGED
@@ -129,8 +129,6 @@ You can also use special values to control access to orgs:
129
129
  tools: {},
130
130
  },
131
131
  }, { telemetry: this.telemetry });
132
- // // TODO: Should we add annotations to our tools? https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure
133
- // // TODO: Move tool names into a shared file, that way if we reference them in multiple places, we can update them in one place
134
132
  const enabledToolsets = new Set(flags.toolsets);
135
133
  const all = enabledToolsets.has('all');
136
134
  // ************************
@@ -8,4 +8,4 @@ export declare function textResponse(text: string, isError?: boolean): ToolTextR
8
8
  * @returns A Set of enabled toolsets
9
9
  */
10
10
  export declare function getEnabledToolsets(availableToolsets: string[], toolsetsInput: string): Set<string>;
11
- export declare function sanitizePath(path: string): boolean;
11
+ export declare function sanitizePath(projectPath: string): boolean;
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  /* eslint-disable no-console */
17
- import { sep } from 'node:path';
17
+ import path from 'node:path';
18
18
  // TODO: break into two helpers? One for errors and one for success?
19
19
  export function textResponse(text, isError = false) {
20
20
  if (text === '')
@@ -52,20 +52,20 @@ export function getEnabledToolsets(availableToolsets, toolsetsInput) {
52
52
  console.error('Enabling toolsets:', Array.from(enabledToolsets).join(', '));
53
53
  return enabledToolsets;
54
54
  }
55
- export function sanitizePath(path) {
55
+ export function sanitizePath(projectPath) {
56
56
  // Decode URL-encoded sequences
57
- const decodedPath = decodeURIComponent(path);
57
+ const decodedProjectPath = decodeURIComponent(projectPath);
58
58
  // Normalize Unicode characters
59
- const normalizedPath = decodedPath.normalize();
59
+ const normalizedProjectPath = decodedProjectPath.normalize();
60
60
  // Check for various traversal patterns
61
- const hasTraversal = normalizedPath.includes('..') ||
62
- normalizedPath.includes('\\..') ||
63
- normalizedPath.includes('../') ||
64
- normalizedPath.includes('..\\') ||
65
- normalizedPath.includes('\u2025') || // Unicode horizontal ellipsis
66
- normalizedPath.includes('\u2026'); // Unicode vertical ellipsis
67
- // Ensure path is absolute
68
- const isAbsolute = path.startsWith(sep);
61
+ const hasTraversal = normalizedProjectPath.includes('..') ||
62
+ normalizedProjectPath.includes('\u2025') || // Unicode horizontal ellipsis
63
+ normalizedProjectPath.includes('\u2026'); // Unicode vertical ellipsis
64
+ // `path.isAbsolute` doesn't cover Windows's drive-relative path:
65
+ // https://github.com/nodejs/node/issues/56766
66
+ //
67
+ // we can assume it's a drive-relative path if it's starts with `\`.
68
+ const isAbsolute = path.isAbsolute(projectPath) && (process.platform === 'win32' ? !projectPath.startsWith('\\') : true);
69
69
  return !hasTraversal && isAbsolute;
70
70
  }
71
71
  //# sourceMappingURL=utils.js.map
@@ -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();
@@ -1,2 +1,2 @@
1
- export * from './sf-run-apex-tests.js';
2
- export * from './sf-run-agent-tests.js';
1
+ export * from './sf-test-apex.js';
2
+ export * from './sf-test-agents.js';
@@ -13,6 +13,6 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export * from './sf-run-apex-tests.js';
17
- export * from './sf-run-agent-tests.js';
16
+ export * from './sf-test-apex.js';
17
+ export * from './sf-test-agents.js';
18
18
  //# sourceMappingURL=index.js.map
@@ -39,7 +39,7 @@ const runAgentTestsParam = z.object({
39
39
  * - textResponse: Test result.
40
40
  */
41
41
  export const registerToolRunAgentTest = (server) => {
42
- server.tool('sf-run-agent-tests', `Run Agent tests in an org.
42
+ server.tool('sf-test-agents', `Run Agent tests in an org.
43
43
 
44
44
  AGENT INSTRUCTIONS:
45
45
  If the user doesn't specify what to test, take context from the currently open file
@@ -50,7 +50,10 @@ this should be chosen when a file in the 'aiEvaluationDefinitions' directory is
50
50
  EXAMPLE USAGE:
51
51
  Run tests for the X agent
52
52
  Run this test
53
- `, runAgentTestsParam.shape, async ({ usernameOrAlias, agentApiName, directory }) => {
53
+ `, runAgentTestsParam.shape, {
54
+ title: 'Run Agent Tests',
55
+ openWorldHint: false,
56
+ }, async ({ usernameOrAlias, agentApiName, directory }) => {
54
57
  if (!usernameOrAlias)
55
58
  return textResponse('The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool', true);
56
59
  // needed for org allowlist to work
@@ -67,4 +70,4 @@ Run this test
67
70
  }
68
71
  });
69
72
  };
70
- //# sourceMappingURL=sf-run-agent-tests.js.map
73
+ //# sourceMappingURL=sf-test-agents.js.map
@@ -30,7 +30,7 @@ RunSpecifiedTests="Run the Apex tests I specify, these will be specified in the
30
30
  `),
31
31
  classNames: z.array(z.string()).describe(`Apex tests classes to run.
32
32
  if Running all tests, all tests should be listed
33
- if unsure, find apex classes matching the pattern *.cls, that include the @isTest decorator in the file and then join their test names together with ','
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
34
  `),
35
35
  usernameOrAlias: usernameOrAliasParam,
36
36
  directory: directoryParam,
@@ -48,7 +48,7 @@ RunSpecifiedTests="Run the Apex tests I specify, these will be specified in the
48
48
  * - textResponse: Test result.
49
49
  */
50
50
  export const registerToolRunApexTest = (server) => {
51
- server.tool('sf-run-apex-tests', `Run Apex tests in an org.
51
+ server.tool('sf-test-apex', `Run Apex tests in an org.
52
52
 
53
53
  AGENT INSTRUCTIONS:
54
54
  If the user doesn't specify what to test, take context from the currently open file
@@ -58,9 +58,12 @@ this should be chosen when a file in the 'classes' directory is mentioned
58
58
 
59
59
  EXAMPLE USAGE:
60
60
  Run tests A, B, C.
61
- Run the tests, find apex classes matching the pattern *.cls, that include the @isTest decorator in the file and then join their test names together with ','
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
62
  Run all tests in the org.
63
- `, runApexTestsParam.shape, async ({ testLevel, usernameOrAlias, classNames, directory }) => {
63
+ `, runApexTestsParam.shape, {
64
+ title: 'Apex Tests',
65
+ openWorldHint: false,
66
+ }, async ({ testLevel, usernameOrAlias, classNames, directory }) => {
64
67
  if (testLevel !== "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */ && classNames?.length && classNames?.length >= 1) {
65
68
  return textResponse("You can't specify which tests to run without setting testLevel='RunSpecifiedTests'", true);
66
69
  }
@@ -80,4 +83,4 @@ Run all tests in the org.
80
83
  }
81
84
  });
82
85
  };
83
- //# sourceMappingURL=sf-run-apex-tests.js.map
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);