diff --git a/CHANGELOG.md b/CHANGELOG.md index 751ac68..97ca33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2025-08-31 + +### Added + +- **Project Deployment Tool**: New tool for deploying metadata to Salesforce orgs + - `deploy_start` - Deploy metadata components to target org with extensive configuration options + - Supports multiple deployment sources: manifest (package.xml), metadata components, source directories + - Configurable test execution levels (NoTestRun, RunSpecifiedTests, RunLocalTests, RunAllTestsInOrg) + - Dry-run capability for validation without actual deployment + - Support for single package deployments + - Respects READ_ONLY and ALLOWED_ORGS permissions + - Integrated with existing permission system for secure deployments + ## [1.3.2] - 2025-08-31 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index b4f54c8..b953beb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,4 +136,5 @@ The project supports Desktop Extension (.dxt) packaging for one-click installati - Check that new tools are registered in both the tool file and manifest.json - Verify permission checks are implemented for destructive operations -- don't forget to add shebang to the build file after creating a new build and before publishing it. it's important \ No newline at end of file +- don't forget to add shebang to the build file after creating a new build and before publishing it. it's important +- when updating documentation, update the @manifest.json as well \ No newline at end of file diff --git a/manifest.json b/manifest.json index a4e5347..b6c161b 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "dxt_version": "0.2", "name": "salesforce-mcp-server", "display_name": "Salesforce MCP Server", - "version": "1.3.0", + "version": "1.4.0", "description": "Salesforce MCP Server - Interact with Salesforce orgs through AI assistants", "icon": "icon.png", "long_description": "Enables AI assistants to execute Apex code, query Salesforce data, and manage org metadata using your existing Salesforce CLI authentication. Perfect for developers and administrators who want to automate Salesforce tasks through natural language interactions.\n\nSupports environment variables:\n- READ_ONLY=true - Prevents Apex code execution\n- ALLOWED_ORGS=ALL or comma-separated org list - Restricts access to specific orgs (default: ALL)", @@ -187,6 +187,14 @@ { "name": "delete_record", "description": "Delete a record from a Salesforce org using REST API" + }, + { + "name": "generate_component", + "description": "Generate Lightning Web Components (LWC) and Aura components with configurable templates" + }, + { + "name": "deploy_start", + "description": "Deploy metadata components to a Salesforce org with test execution options" } ], "compatibility": { diff --git a/package-lock.json b/package-lock.json index 706d3b0..20897e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@advanced-communities/salesforce-mcp-server", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@advanced-communities/salesforce-mcp-server", - "version": "1.3.0", + "version": "1.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.3", diff --git a/package.json b/package.json index 3508cf4..af08539 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@advanced-communities/salesforce-mcp-server", - "version": "1.3.2", + "version": "1.4.0", "description": "MCP server enabling AI assistants to interact with Salesforce orgs through the Salesforce CLI", "main": "./src/index.ts", "scripts": { diff --git a/src/index.ts b/src/index.ts index 03f9e27..865a1a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { registerPackageTools } from "./tools/package.js"; import { registerSchemaTools } from "./tools/schema.js"; import { registerSearchTools } from "./tools/search.js"; import { registerLightningTools } from "./tools/lightning.js"; +import { registerProjectTools } from "./tools/project.js"; import { permissions } from "./config/permissions.js"; /** @@ -24,7 +25,7 @@ function buildServerDescription(): string { const allowedOrgs = permissions.getAllowedOrgs(); const permissionInfo = []; - let description = `Salesforce MCP Server v1.3.2 - AI-powered Salesforce automation via CLI integration\n`; + let description = `Salesforce MCP Server v1.4.0 - AI-powered Salesforce automation via CLI integration\n`; description += `Capabilities: Apex execution, SOQL queries, org management, code testing & coverage\n`; if (readOnlyMode) { @@ -41,7 +42,7 @@ function buildServerDescription(): string { description += `Security: Full access enabled for all authenticated orgs`; } - description += `\nTools: 29 available (apex, query, search, sobject, org management, records, admin, code analyzer, scanner, package, schema)`; + description += `\nTools: 38 available (apex, query, search, sobject, org management, records, admin, code analyzer, scanner, package, schema, lightning, project deployment)`; return description; } @@ -67,6 +68,7 @@ registerPackageTools(server); registerSchemaTools(server); registerSearchTools(server); registerLightningTools(server); +registerProjectTools(server); async function main() { const transport = new StdioServerTransport(); diff --git a/src/tools/project.ts b/src/tools/project.ts new file mode 100644 index 0000000..4a0684d --- /dev/null +++ b/src/tools/project.ts @@ -0,0 +1,183 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { permissions } from "../config/permissions.js"; +import { executeSfCommand, executeSfCommandRaw } from "../utils/sfCommand.js"; + +const deployStart = async ( + targetOrg: string, + dryRun: boolean, + manifest: string, + metadata: string, + metadataDirectory: string, + singlePackage: boolean, + sourceDirectory: string, + tests: string, + testLevel: string +) => { + let sfCommand = `sf project deploy start --target-org ${targetOrg} --json `; + + if (dryRun) { + sfCommand += `--dry-run `; + } + + if (manifest && manifest.length > 0) { + sfCommand += `--manifest ${manifest} `; + } + + if (metadata && metadata.length > 0) { + sfCommand += `--metadata ${metadata} `; + } + + if (metadataDirectory && metadataDirectory.length > 0) { + sfCommand += `--metadata-dir ${metadataDirectory} `; + } + + if (singlePackage) { + sfCommand += `--single-package `; + } + + if (sourceDirectory && sourceDirectory.length > 0) { + sfCommand += `--source-dir ${sourceDirectory} `; + } + + if (tests && tests.length > 0) { + sfCommand += `--tests ${tests} `; + } + + if (testLevel && testLevel.length > 0) { + sfCommand += `--test-level ${testLevel} `; + } + + try { + const result = await executeSfCommand(sfCommand); + return result; + } catch (error) { + throw error; + } +}; + +export const registerProjectTools = (server: McpServer) => { + server.tool( + "deploy_start", + "", + { + input: z.object({ + targetOrg: z + .string() + .describe( + "Username or alias of the target org. Not required if the 'target-org' configuration variable is already set." + ), + dryRun: z + .boolean() + .describe( + "Validate deploy and run Apex tests but don't save to the org." + ), + manifest: z + .string() + .optional() + .describe( + "Full file path for manifest (package.xml) of components to deploy. All child components are included. If you specify this flag, don’t specify --metadata or --source-dir." + ), + metadata: z + .string() + .optional() + .describe( + "Metadata component names to deploy. Wildcards (`*` ) supported as long as you use quotes, such as `ApexClass:MyClass*`." + ), + metadataDirectory: z + .string() + .optional() + .describe( + "Root of directory or zip file of metadata formatted files to deploy." + ), + singlePackage: z + .boolean() + .optional() + .describe( + "Indicates that the metadata zip file points to a directory structure for a single package." + ), + sourceDirectory: z + .string() + .optional() + .describe( + "Path to the local source files to deploy. The supplied path can be to a single file (in which case the operation is applied to only one file) or to a folder (in which case the operation is applied to all metadata types in the directory and its subdirectories). If you specify this flag, don’t specify --metadata or --manifest." + ), + tests: z + .string() + .optional() + .describe( + 'Apex tests to run when --test-level is RunSpecifiedTests. If a test name contains a space, enclose it in double quotes. For multiple test names, use one of the following formats: - Repeat the flag for multiple test names: --tests Test1 --tests Test2 --tests "Test With Space" - Separate the test names with spaces: --tests Test1 Test2 "Test With Space"' + ), + testLevel: z + .string() + .optional() + .describe( + "Deployment Apex testing level. Valid values are: - NoTestRun — No tests are run. This test level applies only to deployments to development environments, such as sandbox, Developer Edition, or trial orgs. This test level is the default for development environments. - RunSpecifiedTests — Runs only the tests that you specify with the --tests flag. Code coverage requirements differ from the default coverage requirements when using this test level. Executed tests must comprise a minimum of 75% code coverage for each class and trigger in the deployment package. This coverage is computed for each class and trigger individually and is different than the overall coverage percentage. - RunLocalTests — All tests in your org are run, except the ones that originate from installed managed and unlocked packages. This test level is the default for production deployments that include Apex classes or triggers. - RunAllTestsInOrg — All tests in your org are run, including tests of managed packages.If you don’t specify a test level, the default behavior depends on the contents of your deployment package and target org." + ), + }), + }, + async ({ input }) => { + const { + targetOrg, + dryRun, + manifest, + metadata, + metadataDirectory, + singlePackage, + sourceDirectory, + tests, + testLevel, + } = input; + + if (permissions.isReadOnly()) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + compiled: false, + compileProblem: + "Operation not allowed: Cannot generate components in READ_ONLY mode", + }), + }, + ], + }; + } + + if (!permissions.isOrgAllowed(targetOrg)) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + success: false, + message: `Access denied: Org '${targetOrg}' is not in the allowed list`, + }), + }, + ], + }; + } + + const result = await deployStart( + targetOrg, + dryRun, + manifest || "", + metadata || "", + metadataDirectory || "", + singlePackage || false, + sourceDirectory || "", + tests || "", + testLevel || "" + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }; + } + ); +};