From 2b722778771f62439f6307e4548d8765126ef12e Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 3 Nov 2025 15:27:57 -0800 Subject: [PATCH 01/18] feat: add ignoreConflict param to deploy --- packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index dd0460b9..84eeeed5 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -35,12 +35,14 @@ import { textResponse } from '../shared/utils.js'; * - apexTests: Apex tests classes to run. * - usernameOrAlias: Username or alias of the Salesforce org to deploy to. * - directory: Directory of the local project. + * - ignoreConflicts: Whether to ignore conflicts during deployment. * * Returns: * - textResponse: Deploy result. */ export const deployMetadataParams = z.object({ + ignoreConflicts: z.boolean().describe('Whether to ignore conflicts during deployment.').optional(), sourceDir: z .array(z.string()) .describe('Path to the local source files to deploy. Leave this unset if the user is vague about what to deploy.') @@ -106,13 +108,14 @@ export class DeployMetadataMcpTool extends McpTool Date: Tue, 4 Nov 2025 14:49:51 -0800 Subject: [PATCH 02/18] feat: add ignoreConflicts params for retrieval --- .../src/tools/deploy_metadata.ts | 6 +-- .../src/tools/retrieve_metadata.ts | 13 +++++- .../test/e2e/deploy_metadata.test.ts | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index 84eeeed5..18fad34c 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -35,14 +35,14 @@ import { textResponse } from '../shared/utils.js'; * - apexTests: Apex tests classes to run. * - usernameOrAlias: Username or alias of the Salesforce org to deploy to. * - directory: Directory of the local project. - * - ignoreConflicts: Whether to ignore conflicts during deployment. + * - ignoreConflicts: Ignore conflicts and deploy local files, even if they overwrite changes in the org. * * Returns: * - textResponse: Deploy result. */ export const deployMetadataParams = z.object({ - ignoreConflicts: z.boolean().describe('Whether to ignore conflicts during deployment.').optional(), + ignoreConflicts: z.boolean().describe(' Ignore conflicts and deploy local files, even if they overwrite changes in the org.').optional(), sourceDir: z .array(z.string()) .describe('Path to the local source files to deploy. Leave this unset if the user is vague about what to deploy.') @@ -115,7 +115,7 @@ Deploy changes to my org Deploy this file to my org Deploy the manifest Deploy X metadata to my org -Deploy X local files to my org and ignore any conflicts between the local project and the org +Deploy X local files to my org and ignore any conflicts between the local project and org Deploy X to my org and run A,B and C apex tests.`, inputSchema: deployMetadataParams.shape, outputSchema: undefined, diff --git a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts index ba9778b3..1cddf15e 100644 --- a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts @@ -33,12 +33,19 @@ import { textResponse } from '../shared/utils.js'; * - manifest: Full file path for manifest (XML file) of components to retrieve. * - usernameOrAlias: Username or alias of the Salesforce org to retrieve from. * - directory: Directory of the local project. + * - ignoreConflicts: Ignore conflicts and retrieve and save files to your local filesystem, even if they overwrite your local changes. * * Returns: * - textResponse: Retrieve result. */ export const retrieveMetadataParams = z.object({ + ignoreConflicts: z.boolean() + .describe( + 'Ignore conflicts and retrieve and save files to your local filesystem, even if they overwrite your local changes.', + ) + .optional() + .default(false), sourceDir: z .array(z.string()) .describe( @@ -77,14 +84,15 @@ export class RetrieveMetadataMcpTool extends McpTool { expect(deployResult.numberTestsCompleted).to.equal(11); expect(deployResult.runTestsEnabled).to.be.true; }); + + it('should deploy a single apex class when ignoreConflicts is true', async () => { + const apexClassPath = path.join( + testSession.project.dir, + 'force-app', + 'main', + 'default', + 'classes', + 'PropertyController.cls', + ); + + const result = await client.callTool(deployMetadataSchema, { + name: 'deploy_metadata', + params: { + sourceDir: [apexClassPath], + ignoreConflicts: true, + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(result.isError).to.equal(false); + expect(result.content.length).to.equal(1); + if (result.content[0].type !== 'text') assert.fail(); + + const responseText = result.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 deployResult = JSON.parse(deployMatch![1]) as { + success: boolean; + done: boolean; + numberComponentsDeployed: number; + }; + expect(deployResult.success).to.be.true; + expect(deployResult.done).to.be.true; + expect(deployResult.numberComponentsDeployed).to.equal(1); + }); }); From ba6c8d277b5b5c848bf864abcdb5a789add4a196 Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 4 Nov 2025 15:25:58 -0800 Subject: [PATCH 03/18] feat: add test for retrieval --- .../test/e2e/retrieve_metadata.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts index ef34d4ad..9b04ba73 100644 --- a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts @@ -182,4 +182,49 @@ describe('retrieve_metadata', () => { expect(apexClasses.length).to.equal(9); expect(packageXml.length).to.equal(1); }); + + it('should retrieve when ignoreConflicts is true', async () => { + const apexClassPath = path.join('force-app', 'main', 'default', 'classes', 'GeocodingService.cls'); + + const result = await client.callTool(retrieveMetadataSchema, { + name: 'retrieve_metadata', + params: { + sourceDir: [apexClassPath], + ignoreConflicts: true, + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(result.isError).to.equal(false); + expect(result.content.length).to.equal(1); + if (result.content[0].type !== 'text') assert.fail(); + + const responseText = result.content[0].text; + expect(responseText).to.contain('Retrieve result:'); + + // Parse the retrieve result JSON + const retrieveMatch = responseText.match(/Retrieve result: ({.*})/); + expect(retrieveMatch).to.not.be.null; + + const retrieveResult = JSON.parse(retrieveMatch![1]) as { + success: boolean; + done: boolean; + fileProperties: Array<{ + type: string; + fullName: string; + fileName: string; + }>; + }; + expect(retrieveResult.success).to.be.true; + expect(retrieveResult.done).to.be.true; + expect(retrieveResult.fileProperties.length).to.equal(2); + + const apexClass = retrieveResult.fileProperties.find( + (fp: { type: string; fullName: string }) => fp.type === 'ApexClass', + ); + if (!apexClass) assert.fail(); + expect(apexClass.fullName).to.equal('GeocodingService'); + expect(apexClass.fileName).to.equal('unpackaged/classes/GeocodingService.cls'); + }); }); From 5a3310af2586b9d5795243bf1d20060bf320af1c Mon Sep 17 00:00:00 2001 From: soridalac Date: Thu, 6 Nov 2025 14:46:17 -0800 Subject: [PATCH 04/18] chore: update e2e for deploy --- .../test/e2e/deploy_metadata.test.ts | 135 ++++++++++++++++-- 1 file changed, 121 insertions(+), 14 deletions(-) diff --git a/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts b/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts index 057e7f42..4304ed54 100644 --- a/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts @@ -15,10 +15,12 @@ */ import path from 'node:path'; +import { promises as fs } from 'node:fs'; import { expect, assert } from 'chai'; import { McpTestClient, DxMcpTransport } from '@salesforce/mcp-test-client'; import { TestSession } from '@salesforce/cli-plugins-testkit'; import { z } from 'zod'; +import { AuthInfo, Connection } from '@salesforce/core'; import { ensureString } from '@salesforce/ts-types'; import { deployMetadataParams } from '../../src/tools/deploy_metadata.js'; @@ -228,17 +230,39 @@ describe('deploy_metadata', () => { expect(deployResult.runTestsEnabled).to.be.true; }); - it('should deploy a single apex class when ignoreConflicts is true', async () => { + it('should deploy local edit when ignoreConflicts is set to true', async () => { const apexClassPath = path.join( testSession.project.dir, 'force-app', 'main', 'default', 'classes', - 'PropertyController.cls', + 'GeocodingService.cls', ); - const result = await client.callTool(deployMetadataSchema, { + // Deploy baseline + const baseline = await client.callTool(deployMetadataSchema, { + name: 'deploy_metadata', + params: { + sourceDir: [apexClassPath], + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(baseline.isError).to.be.false; + expect(baseline.content.length).to.equal(1); + + // Make a local edit + const baselineContent = await fs.readFile(apexClassPath, 'utf8'); + const localEdited = baselineContent.replace( + /(public\s+(?:with\s+sharing\s+)?class\s+GeocodingService[^{]*\{)/, + '$1\n // Local edit', + ); + await fs.writeFile(apexClassPath, localEdited, 'utf8'); + + // Deploy the local change with ignoreConflicts true + const deployResult = await client.callTool(deployMetadataSchema, { name: 'deploy_metadata', params: { sourceDir: [apexClassPath], @@ -248,24 +272,107 @@ describe('deploy_metadata', () => { }, }); - expect(result.isError).to.equal(false); - expect(result.content.length).to.equal(1); - if (result.content[0].type !== 'text') assert.fail(); + expect(deployResult.isError).to.equal(false); + expect(deployResult.content.length).to.equal(1); + if (deployResult.content[0].type !== 'text') assert.fail(); - const responseText = result.content[0].text; - expect(responseText).to.contain('Deploy result:'); + const deployText = deployResult.content[0].text; + expect(deployText).to.contain('Deploy result:'); - // Parse the deploy result JSON - const deployMatch = responseText.match(/Deploy result: ({.*})/); + const deployMatch = deployText.match(/Deploy result: ({.*})/); expect(deployMatch).to.not.be.null; - const deployResult = JSON.parse(deployMatch![1]) as { + const result = JSON.parse(deployMatch![1]) as { success: boolean; done: boolean; numberComponentsDeployed: number; }; - expect(deployResult.success).to.be.true; - expect(deployResult.done).to.be.true; - expect(deployResult.numberComponentsDeployed).to.equal(1); + + expect(result.success).to.be.true; + expect(result.done).to.be.true; + expect(result.numberComponentsDeployed).to.equal(1); + }); + + 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', + ); + + // deploy the whole project to ensure the file exists + const fullProjectDeploy = await client.callTool(deployMetadataSchema, { + name: 'deploy_metadata', + params: { + usernameOrAlias: orgUsername, + directory: testSession.project.dir, + }, + }); + + expect(fullProjectDeploy.isError).to.be.false; + expect(fullProjectDeploy.content.length).to.equal(1); + + // Make a 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, + } + ); + + const updatedMetadata = { + ...customApp.Metadata, + description: customApp.Metadata.description + ? `${customApp.Metadata.description} - Remote edit via Tooling API` + : 'Remote edit via Tooling API', + }; + + await conn.tooling.sobject('CustomApplication').update({ + Id: customApp.Id, + Metadata: updatedMetadata, + }); + + // Deploy with ignoreConflicts=true - should override remote edit + 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.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:'); + + const deployMatch = deployText.match(/Deploy result: ({.*})/); + expect(deployMatch).to.not.be.null; + + const result = 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); }); }); From ca9557d3fce474e4e5e3e4c7f12e9ea41cfdc3cc Mon Sep 17 00:00:00 2001 From: pikuskd <63803197+pikuskd@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:59:00 +0530 Subject: [PATCH 05/18] fix: add basic telemetry to `list_devops_projects` tool (#320) * Add telemetry to list_devops_projects tool * Address PR feedback: Use throw pattern and remove PII - Convert fetchProjects to throw errors instead of returning them - Remove duplicate telemetry tracking (eliminates instanceof check) - Remove username field from telemetry events (PII concern) - Update tests to match throw pattern behavior - Simplifies error handling from 3 paths to 2 paths (-19 lines) --------- Co-authored-by: Manan Dey --- packages/mcp-provider-devops/src/constants.ts | 18 +++++ .../mcp-provider-devops/src/getProjects.ts | 14 ++-- .../src/tools/sfDevopsListProjects.ts | 45 +++++++++-- .../test/getProjects.test.ts | 5 +- .../test/sfDevopsListProjects.test.ts | 81 +++++++++++++++++++ 5 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 packages/mcp-provider-devops/src/constants.ts create mode 100644 packages/mcp-provider-devops/test/sfDevopsListProjects.test.ts diff --git a/packages/mcp-provider-devops/src/constants.ts b/packages/mcp-provider-devops/src/constants.ts new file mode 100644 index 00000000..ba682729 --- /dev/null +++ b/packages/mcp-provider-devops/src/constants.ts @@ -0,0 +1,18 @@ +/** + * Constants for DevOps provider telemetry + */ + +export const TelemetryEventNames = { + LIST_PROJECTS: 'devops_list_projects', + LIST_WORK_ITEMS: 'devops_list_work_items', + PROMOTE_WORK_ITEM: 'devops_promote_work_item', + CHECKOUT_WORK_ITEM: 'devops_checkout_work_item', + COMMIT_WORK_ITEM: 'devops_commit_work_item', + CHECK_COMMIT_STATUS: 'devops_check_commit_status', + CREATE_PULL_REQUEST: 'devops_create_pull_request', + DETECT_CONFLICT: 'devops_detect_conflict', + RESOLVE_CONFLICT: 'devops_resolve_conflict', +} as const; + +export const TelemetrySource = 'MCP-DevOps'; + diff --git a/packages/mcp-provider-devops/src/getProjects.ts b/packages/mcp-provider-devops/src/getProjects.ts index 713f61d9..cef42a9a 100644 --- a/packages/mcp-provider-devops/src/getProjects.ts +++ b/packages/mcp-provider-devops/src/getProjects.ts @@ -6,13 +6,9 @@ export interface DevopsProjectRecord { Description?: string; } -export async function fetchProjects(username: string): Promise { - try { - const connection = await getConnection(username); - const query = "SELECT Id, Name, Description FROM DevopsProject"; - const result = await connection.query(query); - return result.records ?? []; - } catch (error) { - return error; - } +export async function fetchProjects(username: string): Promise { + const connection = await getConnection(username); + const query = "SELECT Id, Name, Description FROM DevopsProject"; + const result = await connection.query(query); + return result.records ?? []; } diff --git a/packages/mcp-provider-devops/src/tools/sfDevopsListProjects.ts b/packages/mcp-provider-devops/src/tools/sfDevopsListProjects.ts index 7edc6cd9..dead5d1d 100644 --- a/packages/mcp-provider-devops/src/tools/sfDevopsListProjects.ts +++ b/packages/mcp-provider-devops/src/tools/sfDevopsListProjects.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { McpTool, McpToolConfig, ReleaseState, Toolset, TelemetryService } from "@salesforce/mcp-provider-api"; import { fetchProjects } from "../getProjects.js"; +import { TelemetryEventNames } from "../constants.js"; const inputSchema = z.object({ username: z.string().describe("Username of the DevOps Center org"), @@ -51,12 +52,42 @@ An array of project records with fields such as Id, Name, Description.`, } public async exec(input: InputArgs): Promise { - const projects = await fetchProjects(input.username); - return { - content: [{ - type: "text", - text: JSON.stringify(projects, null, 2) - }] - }; + const startTime = Date.now(); + + try { + const projects = await fetchProjects(input.username); + + const executionTime = Date.now() - startTime; + const projectCount = projects.length; + + this.telemetryService.sendEvent(TelemetryEventNames.LIST_PROJECTS, { + success: true, + projectCount, + executionTimeMs: executionTime, + }); + + return { + content: [{ + type: "text", + text: JSON.stringify(projects, null, 2) + }] + }; + } catch (error: any) { + const executionTime = Date.now() - startTime; + + this.telemetryService.sendEvent(TelemetryEventNames.LIST_PROJECTS, { + success: false, + error: error?.message || 'Unknown error', + executionTimeMs: executionTime, + }); + + return { + content: [{ + type: "text", + text: `Error fetching projects: ${error?.message || error}` + }], + isError: true + }; + } } } diff --git a/packages/mcp-provider-devops/test/getProjects.test.ts b/packages/mcp-provider-devops/test/getProjects.test.ts index fa13d1bf..69f68707 100644 --- a/packages/mcp-provider-devops/test/getProjects.test.ts +++ b/packages/mcp-provider-devops/test/getProjects.test.ts @@ -23,11 +23,10 @@ describe('fetchProjects', () => { expect(projects).toHaveLength(0); }); - it('should handle errors gracefully', async () => { + it('should throw errors', async () => { const mockConnection = { query: vi.fn().mockRejectedValue(new Error('Network Error')) }; (getConnection as vi.Mock).mockResolvedValue(mockConnection); - const error = await fetchProjects('test-user'); - expect(error.message).toBe('Network Error'); + await expect(fetchProjects('test-user')).rejects.toThrow('Network Error'); }); }); diff --git a/packages/mcp-provider-devops/test/sfDevopsListProjects.test.ts b/packages/mcp-provider-devops/test/sfDevopsListProjects.test.ts new file mode 100644 index 00000000..e4e8ae16 --- /dev/null +++ b/packages/mcp-provider-devops/test/sfDevopsListProjects.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { SfDevopsListProjects } from '../src/tools/sfDevopsListProjects.js'; +import { SpyTelemetryService } from './test-doubles.js'; +import { TelemetryEventNames } from '../src/constants.js'; +import * as getProjectsModule from '../src/getProjects.js'; + +describe('SfDevopsListProjects Telemetry', () => { + let tool: SfDevopsListProjects; + let spyTelemetryService: SpyTelemetryService; + + beforeEach(() => { + spyTelemetryService = new SpyTelemetryService(); + tool = new SfDevopsListProjects(spyTelemetryService); + }); + + it('should send telemetry on successful project fetch', async () => { + const mockProjects = [ + { Id: '1', Name: 'Project 1', Description: 'Test project 1' }, + { Id: '2', Name: 'Project 2', Description: 'Test project 2' }, + ]; + + // Mock the fetchProjects function + vi.spyOn(getProjectsModule, 'fetchProjects').mockResolvedValue(mockProjects); + + const result = await tool.exec({ username: 'test@example.com' }); + + // Verify the result + expect(result.isError).toBeUndefined(); + expect(result.content[0].text).toContain('Project 1'); + + // Verify telemetry was sent + expect(spyTelemetryService.sendEventCallHistory).toHaveLength(1); + + const telemetryEvent = spyTelemetryService.sendEventCallHistory[0]; + expect(telemetryEvent.eventName).toBe(TelemetryEventNames.LIST_PROJECTS); + expect(telemetryEvent.event.success).toBe(true); + expect(telemetryEvent.event.projectCount).toBe(2); + expect(telemetryEvent.event.executionTimeMs).toBeGreaterThanOrEqual(0); + }); + + it('should send telemetry on error', async () => { + const mockError = new Error('Connection failed'); + + // Mock the fetchProjects function to throw an error + vi.spyOn(getProjectsModule, 'fetchProjects').mockRejectedValue(mockError); + + const result = await tool.exec({ username: 'test@example.com' }); + + // Verify the result shows error + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Connection failed'); + + // Verify telemetry was sent with error info + expect(spyTelemetryService.sendEventCallHistory).toHaveLength(1); + + const telemetryEvent = spyTelemetryService.sendEventCallHistory[0]; + expect(telemetryEvent.eventName).toBe(TelemetryEventNames.LIST_PROJECTS); + expect(telemetryEvent.event.success).toBe(false); + expect(telemetryEvent.event.error).toBe('Connection failed'); + expect(telemetryEvent.event.executionTimeMs).toBeGreaterThanOrEqual(0); + }); + + it('should send telemetry with zero count for empty project list', async () => { + // Mock the fetchProjects function to return empty array + vi.spyOn(getProjectsModule, 'fetchProjects').mockResolvedValue([]); + + const result = await tool.exec({ username: 'test@example.com' }); + + // Verify the result + expect(result.isError).toBeUndefined(); + + // Verify telemetry was sent with zero count + expect(spyTelemetryService.sendEventCallHistory).toHaveLength(1); + + const telemetryEvent = spyTelemetryService.sendEventCallHistory[0]; + expect(telemetryEvent.eventName).toBe(TelemetryEventNames.LIST_PROJECTS); + expect(telemetryEvent.event.success).toBe(true); + expect(telemetryEvent.event.projectCount).toBe(0); + }); +}); + From 7a3db632841476c45874584f36b8df2a24942586 Mon Sep 17 00:00:00 2001 From: Sorida Lac <96142310+soridalac@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:56:49 -0800 Subject: [PATCH 06/18] feat: Add e2e test to validate AFV server startup W-19829954 (#315) * feat: add e2e test for AFV * fix: update e2e and reinstall deps * fix: update AFV e2e * chore: Add GH_TOKEN to e2e workflow (#316) * chore: update main/merge * chore: skip test for win --------- Co-authored-by: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Co-authored-by: Willhoit --- .github/workflows/e2e.yml | 1 + .../e2e/agentforce-vibes-mcp-args.test.ts | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/mcp-provider-dx-core/test/e2e/agentforce-vibes-mcp-args.test.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 855c46cc..7f21e481 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -105,4 +105,5 @@ jobs: SF_CHANGE_CASE_CONFIGURATION_ITEM: ${{ secrets.SF_CHANGE_CASE_CONFIGURATION_ITEM}} TESTKIT_SETUP_RETRIES: 2 SF_DISABLE_TELEMETRY: true + GH_TOKEN: ${{ secrets.GH_CLINE_FORK_READ_TOKEN }} DEBUG: ${{ vars.DEBUG }} diff --git a/packages/mcp-provider-dx-core/test/e2e/agentforce-vibes-mcp-args.test.ts b/packages/mcp-provider-dx-core/test/e2e/agentforce-vibes-mcp-args.test.ts new file mode 100644 index 00000000..aaf3c433 --- /dev/null +++ b/packages/mcp-provider-dx-core/test/e2e/agentforce-vibes-mcp-args.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import { expect, assert } from 'chai'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { DxMcpTransport } from '@salesforce/mcp-test-client'; + +const execAsync = promisify(exec); + +async function getMcpClient(opts: { args: string[] }) { + const client = new Client({ + name: 'sf-tools', + version: '0.0.1', + }); + + const transport = DxMcpTransport({ + args: opts.args, + }); + + await client.connect(transport); + + return client; +} + +async function getExpectedArgsAndTools(): Promise<{ args: string[]; tools: string[] }> { + try { + const result = await execAsync('gh api repos/forcedotcom/cline-fork/contents/src/shared/mcp/a4dServerArgs.json | jq -r .content | base64 --decode'); + + if (result.stderr) { + throw new Error(`Command failed: ${result.stderr}`); + } + + const config = JSON.parse(result.stdout.trim()) as Record; + + // Extract args and tools for the MCP repository + const mcpRepoConfig = config['https://github.com/salesforcecli/mcp'] as { args?: string[]; tools?: string[] }; + if (!mcpRepoConfig) { + throw new Error('MCP repository configuration not found in a4dServerArgs.json'); + } + + return { + args: mcpRepoConfig.args ?? [], + tools: mcpRepoConfig.tools ?? [] + }; + } catch (error) { + throw new Error(`Failed to fetch a4dServerArgs.json via gh command: ${String(error)}`); + } +} + +describe('specific tool registration', () => { + // skip only on Windows + const itIf = process.platform === 'win32' ? it.skip : it; + + itIf('should initialize MCP with tools specified in AFV config', async () => { + const { args, tools: expectedTools } = await getExpectedArgsAndTools(); + + const client = await getMcpClient({ + args: [ + ...args, + '--tools', expectedTools.join(','), + '--no-telemetry', + '--allow-non-ga-tools' + ], + }); + + try { + const AllTools = (await client.listTools()).tools.map((t) => t.name).sort(); + expect(AllTools).to.be.an('array').that.is.not.empty; + + // Filter to only include tools that are in the expectedTools list + const initialTools = AllTools.filter(tool => expectedTools.includes(tool)).sort(); + + const missingTools = expectedTools.filter(tool => !initialTools.includes(tool)); + expect(missingTools).to.be.empty; + + expect(initialTools).to.be.an('array'); + expect(initialTools.length).to.deep.equal(expectedTools.length); + + // Verify that each expected tool is loaded + expectedTools.forEach(expectedTool => { + expect(initialTools).to.include(expectedTool); + }); + + // Validate tool names are valid (non-empty strings) + initialTools.forEach(toolName => { + expect(toolName).to.be.a('string').that.is.not.empty; + }); + + } catch (err) { + assert.fail(`Failed to validate tools against a4dServerArgs.json: ${String(err)}`); + } finally { + await client.close(); + } + }); +}); From 06d1cfdff869612dafeaaaa609b019ff9aa09571 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 6 Nov 2025 21:59:55 +0000 Subject: [PATCH 07/18] chore(release): mcp-provider-dx-core@0.4.0 [skip ci] --- packages/mcp-provider-dx-core/CHANGELOG.md | 9 +++++++++ packages/mcp-provider-dx-core/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mcp-provider-dx-core/CHANGELOG.md b/packages/mcp-provider-dx-core/CHANGELOG.md index e8bad40e..099b3248 100644 --- a/packages/mcp-provider-dx-core/CHANGELOG.md +++ b/packages/mcp-provider-dx-core/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.4.0](https://github.com/salesforcecli/mcp/compare/mcp-provider-dx-core@0.3.6...mcp-provider-dx-core@0.4.0) (2025-11-06) + + +### Features + +* Add e2e test to validate AFV server startup W-19829954 ([#315](https://github.com/salesforcecli/mcp/issues/315)) ([6adf542](https://github.com/salesforcecli/mcp/commit/6adf5421a984710e5bb497b017d9958ca67cfc4b)), closes [#316](https://github.com/salesforcecli/mcp/issues/316) + + + ## [0.3.6](https://github.com/salesforcecli/mcp/compare/mcp-provider-dx-core@0.3.5...mcp-provider-dx-core@0.3.6) (2025-10-06) diff --git a/packages/mcp-provider-dx-core/package.json b/packages/mcp-provider-dx-core/package.json index f98656f1..578222d6 100644 --- a/packages/mcp-provider-dx-core/package.json +++ b/packages/mcp-provider-dx-core/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/mcp-provider-dx-core", - "version": "0.3.6", + "version": "0.4.0", "description": "MCP provider for core Salesforce DX functionality", "types": "lib/index.d.ts", "license": "Apache-2.0", From b92eed415ccf6f89900c1e8d6f8c2c2ace132919 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 6 Nov 2025 22:01:30 +0000 Subject: [PATCH 08/18] chore: bump @salesforce/mcp-provider-dx-core to 0.4.0 --no-verify Auto-update dependency after provider package publish. Related release: mcp-provider-dx-core@0.4.0 --- packages/mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 3138c83e..c33ec13b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -44,7 +44,7 @@ "@salesforce/core": "^8.23.1", "@salesforce/kit": "^3.1.6", "@salesforce/mcp-provider-api": "0.4.0", - "@salesforce/mcp-provider-dx-core": "0.3.6", + "@salesforce/mcp-provider-dx-core": "0.4.0", "@salesforce/mcp-provider-code-analyzer": "0.2.0", "@salesforce/mcp-provider-lwc-experts": "0.6.1", "@salesforce/mcp-provider-aura-experts": "0.3.3", From 3b368fbe9388527dd5e3816dfd4f8a4e1485aa30 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 6 Nov 2025 22:16:35 +0000 Subject: [PATCH 09/18] chore(release): mcp-provider-devops@0.1.9 [skip ci] --- packages/mcp-provider-devops/CHANGELOG.md | 11 +++++++++++ packages/mcp-provider-devops/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mcp-provider-devops/CHANGELOG.md b/packages/mcp-provider-devops/CHANGELOG.md index c0fa2307..a8742086 100644 --- a/packages/mcp-provider-devops/CHANGELOG.md +++ b/packages/mcp-provider-devops/CHANGELOG.md @@ -1,3 +1,14 @@ +## [0.1.9](https://github.com/salesforcecli/mcp/compare/mcp-provider-devops@0.1.8...mcp-provider-devops@0.1.9) (2025-11-06) + + +### Bug Fixes + +* add basic telemetry to `list_devops_projects` tool ([#320](https://github.com/salesforcecli/mcp/issues/320)) ([d09cbff](https://github.com/salesforcecli/mcp/commit/d09cbffc06f1ce85f56bc7046d53b74ba42cb8b3)) +* centralized preflight validations for conflict detection into validateAndPrepare and strengthened runtime checks in detectConflict. ([#304](https://github.com/salesforcecli/mcp/issues/304)) ([06816bd](https://github.com/salesforcecli/mcp/commit/06816bd19fa939672ecb266a71427e8315051cdb)) +* commit changes without Sandbox deployment ([#314](https://github.com/salesforcecli/mcp/issues/314)) ([4f7761b](https://github.com/salesforcecli/mcp/commit/4f7761b1536eee75944274f634f1181e269ed099)) + + + ## [0.1.8](https://github.com/salesforcecli/mcp/compare/mcp-provider-devops@0.1.7...mcp-provider-devops@0.1.8) (2025-10-06) diff --git a/packages/mcp-provider-devops/package.json b/packages/mcp-provider-devops/package.json index 8d4d755d..75c345e6 100644 --- a/packages/mcp-provider-devops/package.json +++ b/packages/mcp-provider-devops/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/mcp-provider-devops", "description": "MCP provider for DevOps tools and operations", - "version": "0.1.8", + "version": "0.1.9", "author": "Salesforce", "license": "Apache-2.0", "type": "module", From f26ab58e1d0fa9108f2150036b137ba783675cb3 Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 6 Nov 2025 22:18:14 +0000 Subject: [PATCH 10/18] chore: bump @salesforce/mcp-provider-devops to 0.1.9 --no-verify Auto-update dependency after provider package publish. Related release: mcp-provider-devops@0.1.9 --- packages/mcp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index c33ec13b..e9ebceb0 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -49,7 +49,7 @@ "@salesforce/mcp-provider-lwc-experts": "0.6.1", "@salesforce/mcp-provider-aura-experts": "0.3.3", "@salesforce/mcp-provider-mobile-web": "0.2.0", - "@salesforce/mcp-provider-devops": "0.1.8", + "@salesforce/mcp-provider-devops": "0.1.9", "@salesforce/source-deploy-retrieve": "^12.22.0", "@salesforce/source-tracking": "^7.4.8", "@salesforce/telemetry": "^6.2.6", From 69722d231e1d4aa8df1b5a7ee9f092e1884cdcd4 Mon Sep 17 00:00:00 2001 From: soridalac Date: Fri, 7 Nov 2025 09:01:30 -0800 Subject: [PATCH 11/18] chore: update e2e for retrieval --- .../test/e2e/retrieve_metadata.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts index 9b04ba73..76281db8 100644 --- a/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/retrieve_metadata.test.ts @@ -183,9 +183,16 @@ describe('retrieve_metadata', () => { expect(packageXml.length).to.equal(1); }); - it('should retrieve when ignoreConflicts is true', async () => { + it('should retrieve and overwrite local edits when ignoreConflicts is true', async () => { const apexClassPath = path.join('force-app', 'main', 'default', 'classes', 'GeocodingService.cls'); + const localFilePath = path.join(testSession.project.dir, apexClassPath); + const originalContent = fs.readFileSync(localFilePath, 'utf8'); + const localEditContent = originalContent + '\n// Local edit'; + // Make a local edit to the file + fs.writeFileSync(localFilePath, localEditContent, 'utf8'); + + // Retrieve with ignoreConflicts=true - should overwrite local edit const result = await client.callTool(retrieveMetadataSchema, { name: 'retrieve_metadata', params: { From a2d5cc708ecf9d63f62a67e5d8df9ab688b4215b Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 17 Nov 2025 10:03:15 -0800 Subject: [PATCH 12/18] chore: remove local edit test --- .../test/e2e/deploy_metadata.test.ts | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts b/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts index 4304ed54..97886fd5 100644 --- a/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/deploy_metadata.test.ts @@ -15,7 +15,6 @@ */ import path from 'node:path'; -import { promises as fs } from 'node:fs'; import { expect, assert } from 'chai'; import { McpTestClient, DxMcpTransport } from '@salesforce/mcp-test-client'; import { TestSession } from '@salesforce/cli-plugins-testkit'; @@ -230,69 +229,6 @@ describe('deploy_metadata', () => { expect(deployResult.runTestsEnabled).to.be.true; }); - it('should deploy local edit when ignoreConflicts is set to true', async () => { - const apexClassPath = path.join( - testSession.project.dir, - 'force-app', - 'main', - 'default', - 'classes', - 'GeocodingService.cls', - ); - - // Deploy baseline - const baseline = await client.callTool(deployMetadataSchema, { - name: 'deploy_metadata', - params: { - sourceDir: [apexClassPath], - usernameOrAlias: orgUsername, - directory: testSession.project.dir, - }, - }); - - expect(baseline.isError).to.be.false; - expect(baseline.content.length).to.equal(1); - - // Make a local edit - const baselineContent = await fs.readFile(apexClassPath, 'utf8'); - const localEdited = baselineContent.replace( - /(public\s+(?:with\s+sharing\s+)?class\s+GeocodingService[^{]*\{)/, - '$1\n // Local edit', - ); - await fs.writeFile(apexClassPath, localEdited, 'utf8'); - - // Deploy the local change with ignoreConflicts true - const deployResult = await client.callTool(deployMetadataSchema, { - name: 'deploy_metadata', - params: { - sourceDir: [apexClassPath], - ignoreConflicts: true, - usernameOrAlias: orgUsername, - directory: testSession.project.dir, - }, - }); - - expect(deployResult.isError).to.equal(false); - 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:'); - - const deployMatch = deployText.match(/Deploy result: ({.*})/); - expect(deployMatch).to.not.be.null; - - const result = 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); - }); - it('should deploy remote edit when ignoreConflicts is set to true', async () => { const customAppPath = path.join( testSession.project.dir, From b1db97f00f409376c9e8a8794c4bd928c30612a1 Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 24 Nov 2025 11:42:23 -0800 Subject: [PATCH 13/18] fix: add the error message --- .../src/tools/deploy_metadata.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index 18fad34c..296e0ffb 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -16,7 +16,7 @@ import { z } from 'zod'; import { Connection, Org, SfError, SfProject } from '@salesforce/core'; -import { SourceTracking } from '@salesforce/source-tracking'; +import { SourceTracking, SourceConflictError } from '@salesforce/source-tracking'; import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve'; import { ensureString } from '@salesforce/ts-types'; import { Duration } from '@salesforce/kit'; @@ -166,7 +166,7 @@ Deploy X to my org and run A,B and C apex tests.`, ignoreConflicts: input.ignoreConflicts ?? false, }); - const componentSet = await buildDeployComponentSet(connection, project, stl, input.sourceDir, input.manifest); + const componentSet = await buildDeployComponentSet(connection, project, stl, input.sourceDir, input.manifest, input.ignoreConflicts); if (componentSet.size === 0) { // STL found no changes @@ -209,6 +209,7 @@ async function buildDeployComponentSet( stl: SourceTracking, sourceDir?: string[], manifestPath?: string, + ignoreConflicts?: boolean, ): Promise { if (sourceDir || manifestPath) { return ComponentSetBuilder.build({ @@ -226,8 +227,16 @@ async function buildDeployComponentSet( projectDir: stl?.projectPath, }); } - - // No specific metadata requested to deploy, build component set from STL. - const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry); - return cs; + try { + // No specific metadata requested to deploy, build component set from STL. + const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry); + return cs; + } + catch (error) { + if (ignoreConflicts && error instanceof SourceConflictError) { + // Ignore conflicts as requested, proceed with deployment + return new ComponentSet(undefined, stl.registry); + } + throw new SfError('Failed to build component set for deployment due to conflicts. Set ignoreConflicts=true to override.'); + } } From ed6d52ffc627c91dfaec36bffd727b6bc6eff0fb Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 24 Nov 2025 15:40:08 -0800 Subject: [PATCH 14/18] chore: check for retrieve conflicts --- .../src/tools/retrieve_metadata.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts index 1cddf15e..54e8594c 100644 --- a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts @@ -15,8 +15,8 @@ */ import { z } from 'zod'; -import { Connection, Org, SfProject } from '@salesforce/core'; -import { SourceTracking } from '@salesforce/source-tracking'; +import { Connection, Org, SfError, SfProject } from '@salesforce/core'; +import { SourceConflictError, SourceTracking } from '@salesforce/source-tracking'; import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve'; import { ensureString } from '@salesforce/ts-types'; import { Duration } from '@salesforce/kit'; @@ -136,7 +136,7 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje ignoreConflicts: input.ignoreConflicts ?? false, }); - const componentSet = await buildRetrieveComponentSet(connection, project, stl, input.sourceDir, input.manifest); + const componentSet = await buildRetrieveComponentSet(connection, project, stl, input.sourceDir, input.manifest, input.ignoreConflicts); if (componentSet.size === 0) { // STL found no changes @@ -175,6 +175,7 @@ async function buildRetrieveComponentSet( stl: SourceTracking, sourceDir?: string[], manifestPath?: string, + ignoreConflicts?: boolean, ): Promise { if (sourceDir || manifestPath) { return ComponentSetBuilder.build({ @@ -193,7 +194,14 @@ async function buildRetrieveComponentSet( }); } - // No specific metadata requested to retrieve, build component set from STL. - const cs = await stl.maybeApplyRemoteDeletesToLocal(true); - return cs.componentSetFromNonDeletes; + try { + // No specific metadata requested to retrieve, build component set from STL. + const cs = await stl.maybeApplyRemoteDeletesToLocal(true); + return cs.componentSetFromNonDeletes; + } catch (error) { + if (ignoreConflicts && error instanceof SourceConflictError) { + return await stl.maybeApplyRemoteDeletesToLocal(false); + } + throw new SfError('Failed to build component set for retrieval due to conflicts. Set ignoreConflicts=true to override.'); + } } From 7f5f95699e0afe34457e5236dab3e750cd7fcec4 Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 8 Dec 2025 14:33:05 -0800 Subject: [PATCH 15/18] chore: clear lifecycle singleton pattern --- .../src/tools/deploy_metadata.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index 296e0ffb..44c29ca3 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -15,8 +15,8 @@ */ import { z } from 'zod'; -import { Connection, Org, SfError, SfProject } from '@salesforce/core'; -import { SourceTracking, SourceConflictError } from '@salesforce/source-tracking'; +import { Connection, Lifecycle, 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'; import { Duration } from '@salesforce/kit'; @@ -150,7 +150,7 @@ Deploy X to my org and run A,B and C apex tests.`, const org = await Org.create({ connection }); - if (!input.sourceDir && !input.manifest && !(await org.tracksSource())) { + if (!input.sourceDir && !input.manifest && !input.ignoreConflicts && !(await org.tracksSource())) { return textResponse( 'This org does not have source-tracking enabled or does not support source-tracking. You should specify the files or a manifest to deploy.', true, @@ -159,14 +159,20 @@ 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, + subscribeSDREvents: !input.ignoreConflicts, ignoreConflicts: input.ignoreConflicts ?? false, }); - const componentSet = await buildDeployComponentSet(connection, project, stl, input.sourceDir, input.manifest, input.ignoreConflicts); + const componentSet = await buildDeployComponentSet(connection, project, stl, input.sourceDir, input.manifest); if (componentSet.size === 0) { // STL found no changes @@ -179,6 +185,8 @@ Deploy X to my org and run A,B and C apex tests.`, ...(input.apexTests ? { runTests: input.apexTests, testLevel: 'RunSpecifiedTests' } : {}), ...(input.apexTestLevel ? { testLevel: input.apexTestLevel } : {}), }, + // Only pass tracker for normal deploys; force deploys bypass tracking + ...(!input.ignoreConflicts ? { tracker: stl } : {}), }); jobId = deploy.id ?? ''; @@ -209,7 +217,6 @@ async function buildDeployComponentSet( stl: SourceTracking, sourceDir?: string[], manifestPath?: string, - ignoreConflicts?: boolean, ): Promise { if (sourceDir || manifestPath) { return ComponentSetBuilder.build({ @@ -227,16 +234,8 @@ async function buildDeployComponentSet( projectDir: stl?.projectPath, }); } - try { - // No specific metadata requested to deploy, build component set from STL. - const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry); - return cs; - } - catch (error) { - if (ignoreConflicts && error instanceof SourceConflictError) { - // Ignore conflicts as requested, proceed with deployment - return new ComponentSet(undefined, stl.registry); - } - throw new SfError('Failed to build component set for deployment due to conflicts. Set ignoreConflicts=true to override.'); - } + + const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry); + return cs; } + From 5aa8cb2a58231634967bc0d2e5841a0e377b9a02 Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 8 Dec 2025 14:57:50 -0800 Subject: [PATCH 16/18] chore: clear old lifecycle listener for retrieve --- .../src/tools/deploy_metadata.ts | 3 +- .../src/tools/retrieve_metadata.ts | 31 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index 44c29ca3..86eb6e53 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -234,7 +234,8 @@ async function buildDeployComponentSet( projectDir: stl?.projectPath, }); } - + + // No specific metadata requested to deploy, build component set from STL. const cs = (await stl.localChangesAsComponentSet(false))[0] ?? new ComponentSet(undefined, stl.registry); return cs; } diff --git a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts index 54e8594c..29a1dbed 100644 --- a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts @@ -15,8 +15,8 @@ */ import { z } from 'zod'; -import { Connection, Org, SfError, SfProject } from '@salesforce/core'; -import { SourceConflictError, SourceTracking } from '@salesforce/source-tracking'; +import { Connection, Lifecycle, 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'; import { Duration } from '@salesforce/kit'; @@ -121,7 +121,7 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje const org = await Org.create({ connection }); - if (!input.sourceDir && !input.manifest && !(await org.tracksSource())) { + if (!input.sourceDir && !input.manifest && !input.ignoreConflicts &&!(await org.tracksSource())) { return textResponse( 'This org does not have source-tracking enabled or does not support source-tracking. You should specify the files or a manifest to retrieve.', true, @@ -129,15 +129,20 @@ 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: !input.ignoreConflicts, ignoreConflicts: input.ignoreConflicts ?? false, }); - const componentSet = await buildRetrieveComponentSet(connection, project, stl, input.sourceDir, input.manifest, input.ignoreConflicts); - + const componentSet = await buildRetrieveComponentSet(connection, project, stl, input.sourceDir, input.manifest); if (componentSet.size === 0) { // STL found no changes return textResponse('No remote changes to retrieve were found.'); @@ -175,7 +180,6 @@ async function buildRetrieveComponentSet( stl: SourceTracking, sourceDir?: string[], manifestPath?: string, - ignoreConflicts?: boolean, ): Promise { if (sourceDir || manifestPath) { return ComponentSetBuilder.build({ @@ -194,14 +198,7 @@ async function buildRetrieveComponentSet( }); } - try { - // No specific metadata requested to retrieve, build component set from STL. - const cs = await stl.maybeApplyRemoteDeletesToLocal(true); - return cs.componentSetFromNonDeletes; - } catch (error) { - if (ignoreConflicts && error instanceof SourceConflictError) { - return await stl.maybeApplyRemoteDeletesToLocal(false); - } - throw new SfError('Failed to build component set for retrieval due to conflicts. Set ignoreConflicts=true to override.'); - } + // No specific metadata requested to retrieve, build component set from STL. + const cs = await stl.maybeApplyRemoteDeletesToLocal(true); + return cs.componentSetFromNonDeletes; } From 0f45ec47e107b9860419474836dac112974604c8 Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 9 Dec 2025 10:20:40 -0800 Subject: [PATCH 17/18] chore: set event listener to true --- packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts | 4 +--- packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts index 86eb6e53..2dd34477 100644 --- a/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/deploy_metadata.ts @@ -168,7 +168,7 @@ Deploy X to my org and run A,B and C apex tests.`, const stl = await SourceTracking.create({ org, project, - subscribeSDREvents: !input.ignoreConflicts, + subscribeSDREvents: true, ignoreConflicts: input.ignoreConflicts ?? false, }); @@ -185,8 +185,6 @@ Deploy X to my org and run A,B and C apex tests.`, ...(input.apexTests ? { runTests: input.apexTests, testLevel: 'RunSpecifiedTests' } : {}), ...(input.apexTestLevel ? { testLevel: input.apexTestLevel } : {}), }, - // Only pass tracker for normal deploys; force deploys bypass tracking - ...(!input.ignoreConflicts ? { tracker: stl } : {}), }); jobId = deploy.id ?? ''; diff --git a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts index 29a1dbed..73965d7c 100644 --- a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts @@ -132,13 +132,14 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje // Clear old conflict listeners for force retrieve if (input.ignoreConflicts) { const lifecycle = Lifecycle.getInstance(); + lifecycle.removeAllListeners('scopedPreDeploy'); lifecycle.removeAllListeners('scopedPreRetrieve'); } const stl = await SourceTracking.create({ org, project, - subscribeSDREvents: !input.ignoreConflicts, + subscribeSDREvents: true, ignoreConflicts: input.ignoreConflicts ?? false, }); From 1fc2ce482b6aa0aab036642f9165f4225ed0496d Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 9 Dec 2025 12:37:40 -0800 Subject: [PATCH 18/18] fix: remove redundant --- packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts index 73965d7c..ade95d49 100644 --- a/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts +++ b/packages/mcp-provider-dx-core/src/tools/retrieve_metadata.ts @@ -121,7 +121,7 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje const org = await Org.create({ connection }); - if (!input.sourceDir && !input.manifest && !input.ignoreConflicts &&!(await org.tracksSource())) { + if (!input.sourceDir && !input.manifest && !input.ignoreConflicts && !(await org.tracksSource())) { return textResponse( 'This org does not have source-tracking enabled or does not support source-tracking. You should specify the files or a manifest to retrieve.', true, @@ -132,7 +132,6 @@ Retrieve X metadata from my org and ignore any conflicts between the local proje // Clear old conflict listeners for force retrieve if (input.ignoreConflicts) { const lifecycle = Lifecycle.getInstance(); - lifecycle.removeAllListeners('scopedPreDeploy'); lifecycle.removeAllListeners('scopedPreRetrieve'); }