|
| 1 | +import { z } from "zod"; |
| 2 | +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 3 | +import { permissions } from "../config/permissions.js"; |
| 4 | +import { executeSfCommand, executeSfCommandRaw } from "../utils/sfCommand.js"; |
| 5 | + |
| 6 | +const deployStart = async ( |
| 7 | + targetOrg: string, |
| 8 | + dryRun: boolean, |
| 9 | + manifest: string, |
| 10 | + metadata: string, |
| 11 | + metadataDirectory: string, |
| 12 | + singlePackage: boolean, |
| 13 | + sourceDirectory: string, |
| 14 | + tests: string, |
| 15 | + testLevel: string |
| 16 | +) => { |
| 17 | + let sfCommand = `sf project deploy start --target-org ${targetOrg} --json `; |
| 18 | + |
| 19 | + if (dryRun) { |
| 20 | + sfCommand += `--dry-run `; |
| 21 | + } |
| 22 | + |
| 23 | + if (manifest && manifest.length > 0) { |
| 24 | + sfCommand += `--manifest ${manifest} `; |
| 25 | + } |
| 26 | + |
| 27 | + if (metadata && metadata.length > 0) { |
| 28 | + sfCommand += `--metadata ${metadata} `; |
| 29 | + } |
| 30 | + |
| 31 | + if (metadataDirectory && metadataDirectory.length > 0) { |
| 32 | + sfCommand += `--metadata-dir ${metadataDirectory} `; |
| 33 | + } |
| 34 | + |
| 35 | + if (singlePackage) { |
| 36 | + sfCommand += `--single-package `; |
| 37 | + } |
| 38 | + |
| 39 | + if (sourceDirectory && sourceDirectory.length > 0) { |
| 40 | + sfCommand += `--source-dir ${sourceDirectory} `; |
| 41 | + } |
| 42 | + |
| 43 | + if (tests && tests.length > 0) { |
| 44 | + sfCommand += `--tests ${tests} `; |
| 45 | + } |
| 46 | + |
| 47 | + if (testLevel && testLevel.length > 0) { |
| 48 | + sfCommand += `--test-level ${testLevel} `; |
| 49 | + } |
| 50 | + |
| 51 | + try { |
| 52 | + const result = await executeSfCommand(sfCommand); |
| 53 | + return result; |
| 54 | + } catch (error) { |
| 55 | + throw error; |
| 56 | + } |
| 57 | +}; |
| 58 | + |
| 59 | +export const registerProjectTools = (server: McpServer) => { |
| 60 | + server.tool( |
| 61 | + "deploy_start", |
| 62 | + "", |
| 63 | + { |
| 64 | + input: z.object({ |
| 65 | + targetOrg: z |
| 66 | + .string() |
| 67 | + .describe( |
| 68 | + "Username or alias of the target org. Not required if the 'target-org' configuration variable is already set." |
| 69 | + ), |
| 70 | + dryRun: z |
| 71 | + .boolean() |
| 72 | + .describe( |
| 73 | + "Validate deploy and run Apex tests but don't save to the org." |
| 74 | + ), |
| 75 | + manifest: z |
| 76 | + .string() |
| 77 | + .optional() |
| 78 | + .describe( |
| 79 | + "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." |
| 80 | + ), |
| 81 | + metadata: z |
| 82 | + .string() |
| 83 | + .optional() |
| 84 | + .describe( |
| 85 | + "Metadata component names to deploy. Wildcards (`*` ) supported as long as you use quotes, such as `ApexClass:MyClass*`." |
| 86 | + ), |
| 87 | + metadataDirectory: z |
| 88 | + .string() |
| 89 | + .optional() |
| 90 | + .describe( |
| 91 | + "Root of directory or zip file of metadata formatted files to deploy." |
| 92 | + ), |
| 93 | + singlePackage: z |
| 94 | + .boolean() |
| 95 | + .optional() |
| 96 | + .describe( |
| 97 | + "Indicates that the metadata zip file points to a directory structure for a single package." |
| 98 | + ), |
| 99 | + sourceDirectory: z |
| 100 | + .string() |
| 101 | + .optional() |
| 102 | + .describe( |
| 103 | + "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." |
| 104 | + ), |
| 105 | + tests: z |
| 106 | + .string() |
| 107 | + .optional() |
| 108 | + .describe( |
| 109 | + '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"' |
| 110 | + ), |
| 111 | + testLevel: z |
| 112 | + .string() |
| 113 | + .optional() |
| 114 | + .describe( |
| 115 | + "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." |
| 116 | + ), |
| 117 | + }), |
| 118 | + }, |
| 119 | + async ({ input }) => { |
| 120 | + const { |
| 121 | + targetOrg, |
| 122 | + dryRun, |
| 123 | + manifest, |
| 124 | + metadata, |
| 125 | + metadataDirectory, |
| 126 | + singlePackage, |
| 127 | + sourceDirectory, |
| 128 | + tests, |
| 129 | + testLevel, |
| 130 | + } = input; |
| 131 | + |
| 132 | + if (permissions.isReadOnly()) { |
| 133 | + return { |
| 134 | + content: [ |
| 135 | + { |
| 136 | + type: "text", |
| 137 | + text: JSON.stringify({ |
| 138 | + success: false, |
| 139 | + compiled: false, |
| 140 | + compileProblem: |
| 141 | + "Operation not allowed: Cannot generate components in READ_ONLY mode", |
| 142 | + }), |
| 143 | + }, |
| 144 | + ], |
| 145 | + }; |
| 146 | + } |
| 147 | + |
| 148 | + if (!permissions.isOrgAllowed(targetOrg)) { |
| 149 | + return { |
| 150 | + content: [ |
| 151 | + { |
| 152 | + type: "text", |
| 153 | + text: JSON.stringify({ |
| 154 | + success: false, |
| 155 | + message: `Access denied: Org '${targetOrg}' is not in the allowed list`, |
| 156 | + }), |
| 157 | + }, |
| 158 | + ], |
| 159 | + }; |
| 160 | + } |
| 161 | + |
| 162 | + const result = await deployStart( |
| 163 | + targetOrg, |
| 164 | + dryRun, |
| 165 | + manifest || "", |
| 166 | + metadata || "", |
| 167 | + metadataDirectory || "", |
| 168 | + singlePackage || false, |
| 169 | + sourceDirectory || "", |
| 170 | + tests || "", |
| 171 | + testLevel || "" |
| 172 | + ); |
| 173 | + return { |
| 174 | + content: [ |
| 175 | + { |
| 176 | + type: "text", |
| 177 | + text: JSON.stringify(result), |
| 178 | + }, |
| 179 | + ], |
| 180 | + }; |
| 181 | + } |
| 182 | + ); |
| 183 | +}; |
0 commit comments