Skip to content
5 changes: 2 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"request": "attach",
"name": "Attach to Debug Hook Process",
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/packages/mcp/lib/**/*.js"]
"sourceMaps": true
}
]
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rimraf": "^6.0.1"
},
"overrides": {
"@salesforce/core": "^8.22.0"
"@salesforce/core": "^8.24.3"
},
"scripts": {
"build": "yarn workspaces run build",
Expand Down
2 changes: 1 addition & 1 deletion packages/EXAMPLE-MCP-PROVIDER/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"types": "dist/index.d.ts",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/mcp-provider-api": "^0.4.1",
"zod": "^3.25.76"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-provider-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"types": "dist/index.d.ts",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
"@salesforce/core": "^8",
"@salesforce/core": "^8.24.3",
"@salesforce/ts-types": "^2",
"@types/semver": "^7.7.1",
"semver": "^7.7.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-provider-code-analyzer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@salesforce/code-analyzer-pmd-engine": "^0.33.0",
"@salesforce/code-analyzer-regex-engine": "^0.30.0",
"@salesforce/code-analyzer-retirejs-engine": "^0.29.0",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/mcp-provider-api": "^0.4.1",
"zod": "^3.25.76"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/mcp-provider-devops/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"types": "dist/index.d.ts",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.0",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/core": "^8.17.0",
"@salesforce/source-deploy-retrieve": "^12.22.13",
"@salesforce/mcp-provider-api": "^0.4.1",
"@salesforce/core": "^8.24.3",
"@salesforce/source-deploy-retrieve": "^12.31.7",
"@salesforce/ts-types": "^2.0.12",
"axios": "^1.10.0",
"zod": "^3.25.76"
Expand Down
8 changes: 4 additions & 4 deletions packages/mcp-provider-dx-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@
"@modelcontextprotocol/sdk": "^1.18.0",
"@salesforce/agents": "^0.15.4",
"@salesforce/apex-node": "^8.2.1",
"@salesforce/core": "^8.23.1",
"@salesforce/core": "^8.24.3",
"@salesforce/kit": "^3.1.6",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/source-deploy-retrieve": "^12.22.0",
"@salesforce/source-tracking": "^7.4.8",
"@salesforce/mcp-provider-api": "^0.4.1",
"@salesforce/source-deploy-retrieve": "^12.31.8",
"@salesforce/source-tracking": "^7.8.2",
"@salesforce/ts-types": "^2.0.11",
"open": "^10.1.2",
"zod": "^3.25.76"
Expand Down
15 changes: 6 additions & 9 deletions packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { z } from 'zod';
import { Connection, Lifecycle, Org, SfError, SfProject } from '@salesforce/core';
import { Connection, Org, SfError, SfProject } from '@salesforce/core';
import { SourceTracking } from '@salesforce/source-tracking';
import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve';
import { ensureString } from '@salesforce/ts-types';
Expand Down Expand Up @@ -159,19 +159,16 @@ Deploy X to my org and run A,B and C apex tests.`,

let jobId: string = '';
try {
// Clear old conflict listeners for force deploy
if (input.ignoreConflicts) {
const lifecycle = Lifecycle.getInstance();
lifecycle.removeAllListeners('scopedPreDeploy');
}

const stl = await SourceTracking.create({
org,
project,
subscribeSDREvents: true,
ignoreConflicts: input.ignoreConflicts ?? false,
subscribeSDREvents: true, // Always subscribe for tracking updates (post-deploy)
ignoreConflicts: input.ignoreConflicts ?? false, // Only controls pre-deploy conflict checks
});

// Force refresh of the global ShadowRepo singleton cache to detect new changes
await stl.reReadLocalTrackingCache();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STL caches the git info per project (shadowrepo singleton) so we need to force a reRead here


const componentSet = await buildDeployComponentSet(connection, project, stl, input.sourceDir, input.manifest);

if (componentSet.size === 0) {
Expand Down
15 changes: 6 additions & 9 deletions packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { z } from 'zod';
import { Connection, Lifecycle, Org, SfProject } from '@salesforce/core';
import { Connection, Org, SfProject } from '@salesforce/core';
import { SourceTracking } from '@salesforce/source-tracking';
import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve';
import { ensureString } from '@salesforce/ts-types';
Expand Down Expand Up @@ -129,19 +129,16 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje
}

try {
// Clear old conflict listeners for force retrieve
if (input.ignoreConflicts) {
const lifecycle = Lifecycle.getInstance();
lifecycle.removeAllListeners('scopedPreRetrieve');
}

const stl = await SourceTracking.create({
org,
project,
subscribeSDREvents: true,
subscribeSDREvents: true, // Always subscribe for tracking updates (post-retrieve)
ignoreConflicts: input.ignoreConflicts ?? false,
});

// Force refresh of the global ShadowRepo singleton cache to detect new changes
await stl.reReadLocalTrackingCache();

const componentSet = await buildRetrieveComponentSet(connection, project, stl, input.sourceDir, input.manifest);
if (componentSet.size === 0) {
// STL found no changes
Expand Down Expand Up @@ -181,7 +178,7 @@ async function buildRetrieveComponentSet(
sourceDir?: string[],
manifestPath?: string,
): Promise<ComponentSet> {
if (sourceDir || manifestPath) {
if (sourceDir ?? manifestPath) {
return ComponentSetBuilder.build({
apiversion: connection.getApiVersion(),
sourceapiversion: ensureString((await project.resolveProjectConfig()).sourceApiVersion),
Expand Down
63 changes: 42 additions & 21 deletions packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import fs from 'node:fs';
import path from 'node:path';
import { expect, assert } from 'chai';
import { McpTestClient, DxMcpTransport } from '@salesforce/mcp-test-client';
Expand Down Expand Up @@ -229,16 +230,7 @@ describe('deploy_metadata', () => {
expect(deployResult.runTestsEnabled).to.be.true;
});

it('should deploy remote edit when ignoreConflicts is set to true', async () => {
const customAppPath = path.join(
testSession.project.dir,
'force-app',
'main',
'default',
'applications',
'Dreamhouse.app-meta.xml',
);

it('should deploy remote edit when ignoreConflicts is set to true', async () => {
// deploy the whole project to ensure the file exists
const fullProjectDeploy = await client.callTool(deployMetadataSchema, {
name: 'deploy_metadata',
Expand All @@ -251,6 +243,21 @@ describe('deploy_metadata', () => {
expect(fullProjectDeploy.isError).to.be.false;
expect(fullProjectDeploy.content.length).to.equal(1);

const customAppPath = path.join(
testSession.project.dir,
'force-app',
'main',
'default',
'applications',
'Dreamhouse.app-meta.xml',
);

// local edit
await fs.promises.writeFile(
customAppPath,
(await fs.promises.readFile(customAppPath, { encoding: 'utf-8' })).replace('Lightning App Builder', 'App Builder')
);

// Make a remote edit using Tooling API
const conn = await Connection.create({
authInfo: await AuthInfo.create({ username: orgUsername }),
Expand Down Expand Up @@ -280,35 +287,49 @@ describe('deploy_metadata', () => {
Metadata: updatedMetadata,
});

// Deploy with ignoreConflicts=true - should override remote edit
// Deploy without ignoreConflicts - should throw conflicts error
const deployResult = await client.callTool(deployMetadataSchema, {
name: 'deploy_metadata',
params: {
sourceDir: [customAppPath],
ignoreConflicts: true,
usernameOrAlias: orgUsername,
directory: testSession.project.dir,
},
});

expect(deployResult.isError).to.equal(false);
expect(deployResult.isError).to.equal(true);
expect(deployResult.content.length).to.equal(1);
if (deployResult.content[0].type !== 'text') assert.fail();

const deployText = deployResult.content[0].text;
expect(deployText).to.contain('Deploy result:');
expect(deployText).to.contain('Failed to deploy metadata: 1 conflicts detected');

const deployMatch = deployText.match(/Deploy result: ({.*})/);
// deploying with ignoreConflicts=true should succeed
const ignoreConflictDeployResult = await client.callTool(deployMetadataSchema, {
name: 'deploy_metadata',
params: {
ignoreConflicts: true,
usernameOrAlias: orgUsername,
directory: testSession.project.dir,
},
});

expect(ignoreConflictDeployResult.content.length).to.equal(1);
if (ignoreConflictDeployResult.content[0].type !== 'text') assert.fail();

const responseText = ignoreConflictDeployResult.content[0].text;
expect(responseText).to.contain('Deploy result:');

// Parse the deploy result JSON
const deployMatch = responseText.match(/Deploy result: ({.*})/);
expect(deployMatch).to.not.be.null;

const result = JSON.parse(deployMatch![1]) as {
const ignoreConflictsDeployRes = JSON.parse(deployMatch![1]) as {
success: boolean;
done: boolean;
numberComponentsDeployed: number;
};

expect(result.success).to.be.true;
expect(result.done).to.be.true;
expect(result.numberComponentsDeployed).to.equal(1);
expect(ignoreConflictsDeployRes.success).to.be.true;
expect(ignoreConflictsDeployRes.done).to.be.true;
expect(ignoreConflictsDeployRes.numberComponentsDeployed).to.equal(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { McpTestClient, DxMcpTransport } from '@salesforce/mcp-test-client';
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { z } from 'zod';
import { ensureString } from '@salesforce/ts-types';
import { AuthInfo, Connection } from '@salesforce/core';
import { retrieveMetadataParams } from '../../src/tools/retrieve_metadata.js';

describe('retrieve_metadata', () => {
Expand Down Expand Up @@ -234,4 +235,95 @@ describe('retrieve_metadata', () => {
expect(apexClass.fullName).to.equal('GeocodingService');
expect(apexClass.fileName).to.equal('unpackaged/classes/GeocodingService.cls');
});

it('should retrieve remote edit when ignoreConflicts is set to true', async () => {
// 1. Setup: Use a CustomApplication file
const customAppPath = path.join(
testSession.project.dir,
'force-app',
'main',
'default',
'applications',
'Dreamhouse.app-meta.xml'
);

// 2. Make local edit
await fs.promises.writeFile(
customAppPath,
(await fs.promises.readFile(customAppPath, { encoding: 'utf-8' })).replace('Lightning App Builder', 'App Builder')
);

// 3. Make remote edit using Tooling API
const conn = await Connection.create({
authInfo: await AuthInfo.create({ username: orgUsername }),
});

const customApp = await conn.singleRecordQuery<{
Id: string;
Metadata: { description: string | null };
}>("SELECT Id, Metadata FROM CustomApplication WHERE DeveloperName = 'Dreamhouse'", { tooling: true });

await conn.tooling.sobject('CustomApplication').update({
Id: customApp.Id,
Metadata: {
...customApp.Metadata,
description: customApp.Metadata.description
? `${customApp.Metadata.description} - Remote edit via Tooling API`
: 'Remote edit via Tooling API',
},
});

// 4. Retrieve without ignoreConflicts - should fail with conflicts error
const retrieveResult = await client.callTool(retrieveMetadataSchema, {
name: 'retrieve_metadata',
params: {
usernameOrAlias: orgUsername,
directory: testSession.project.dir,
},
});

expect(retrieveResult.isError).to.equal(true);
expect(retrieveResult.content[0].type).to.equal('text');
if (retrieveResult.content[0].type !== 'text') assert.fail();

const retrieveText = retrieveResult.content[0].text;
expect(retrieveText).to.contain('Failed to retrieve metadata: 1 conflicts detected');

// 5. Retrieve with ignoreConflicts=true - should succeed
const ignoreConflictRetrieveResult = await client.callTool(retrieveMetadataSchema, {
name: 'retrieve_metadata',
params: {
ignoreConflicts: true,
usernameOrAlias: orgUsername,
directory: testSession.project.dir,
},
});

expect(ignoreConflictRetrieveResult.content[0].type).to.equal('text');
const responseText = (ignoreConflictRetrieveResult.content[0] as { text: string }).text;
expect(responseText).to.contain('Retrieve result:');

const retrieveMatch = responseText.match(/Retrieve result: ({.*})/);
expect(retrieveMatch).to.not.be.null;

const ignoreConflictsRetrieveRes = JSON.parse(retrieveMatch![1]) as {
success: boolean;
done: boolean;
fileProperties: Array<{
type: string;
fullName: string;
fileName: string;
}>;
};
expect(ignoreConflictsRetrieveRes.success).to.be.true;
expect(ignoreConflictsRetrieveRes.done).to.be.true;

// Verify the CustomApplication was retrieved
const customAppFile = ignoreConflictsRetrieveRes.fileProperties.find(
(fp) => fp.type === 'CustomApplication'
);
expect(customAppFile).to.not.be.undefined;
expect(customAppFile!.fullName).to.equal('Dreamhouse');
expect(customAppFile!.fileName).to.equal('unpackaged/applications/Dreamhouse.app');
});
});
2 changes: 1 addition & 1 deletion packages/mcp-provider-mobile-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@modelcontextprotocol/sdk": "^1.17.3",
"eslint": "^9.35.0",
"dedent": "^1.5.3",
"@salesforce/mcp-provider-api": "^0.4.0",
"@salesforce/mcp-provider-api": "^0.4.1",
"@salesforce/eslint-plugin-lwc-graph-analyzer": "^1.1.0-beta.2",
"zod": "^3.25.76"
},
Expand Down
10 changes: 5 additions & 5 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@
"@oclif/core": "^4.5.1",
"@salesforce/agents": "^0.15.4",
"@salesforce/apex-node": "^8.2.1",
"@salesforce/core": "^8.23.1",
"@salesforce/core": "^8.24.3",
"@salesforce/kit": "^3.1.6",
"@salesforce/mcp-provider-api": "0.4.1",
"@salesforce/mcp-provider-dx-core": "0.5.1",
"@salesforce/mcp-provider-dx-core": "0.6.0",
"@salesforce/mcp-provider-code-analyzer": "0.4.0",
"@salesforce/mcp-provider-lwc-experts": "0.6.5",
"@salesforce/mcp-provider-aura-experts": "0.3.6",
"@salesforce/mcp-provider-mobile-web": "0.2.2",
"@salesforce/mcp-provider-devops": "0.1.9",
"@salesforce/mcp-provider-scale-products": "0.0.2",
"@salesforce/source-deploy-retrieve": "^12.22.0",
"@salesforce/source-tracking": "^7.4.8",
"@salesforce/telemetry": "6.4.5",
"@salesforce/source-deploy-retrieve": "^12.31.7",
"@salesforce/source-tracking": "^7.8.2",
"@salesforce/telemetry": "6.4.1",
"@salesforce/ts-types": "^2.0.11",
"zod": "^3.25.76"
},
Expand Down
Loading