Deploy a release to environments in Octopus Deploy
deploy_releaseDeploy a release to single or multiple environments in Octopus Deploy. Supports both tenanted and untenanted deployments with configurable options.
Instructions
Deploy a release to one or more environments in Octopus Deploy
This tool supports both tenanted and untenanted deployments:
Untenanted: Don't provide tenants or tenantTags. Can deploy to multiple environments at once.
Tenanted: Provide tenants or tenantTags. Can only deploy to ONE environment, but can target multiple tenants.
The tool automatically determines which deployment type to use based on the parameters provided.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| spaceName | Yes | The space name | |
| projectName | Yes | The project name | |
| releaseVersion | Yes | The release version to deploy (e.g., '1.0.0') | |
| environmentNames | Yes | Array of environment names. For tenanted deployments, must contain exactly one environment. | |
| tenants | No | Array of tenant names for tenanted deployment (optional) | |
| tenantTags | No | Array of tenant tags for tenanted deployment (e.g., ['Region/US-West', 'Tier/Production']) | |
| forcePackageRedeployment | No | Force redeployment of packages | |
| updateVariableSnapshot | No | Update the variable snapshot | |
| forcePackageDownload | No | Force package download | |
| specificMachineNames | No | Deploy to specific machines only | |
| excludedMachineNames | No | Exclude specific machines from deployment | |
| skipStepNames | No | Skip specific deployment steps | |
| useGuidedFailure | No | Use guided failure mode | |
| runAt | No | Schedule deployment for later (ISO 8601 date string) | |
| noRunAfter | No | Don't run deployment after this time (ISO 8601 date string) | |
| variables | No | Prompted variable values as key-value pairs | |
| deploymentFreezeOverrideReason | No | Reason for overriding deployment freeze | |
| deploymentFreezeNames | No | Names of deployment freezes to override | |
| confirm | No | Required only when the MCP client does not support elicitation. Set to true to confirm deployment; otherwise the tool aborts. |
Implementation Reference
- src/tools/deployRelease.ts:105-283 (handler)The main tool handler function that executes the deploy_release logic. It validates inputs (tenanted vs untenanted), builds the deployment command, requires user confirmation, creates the Octopus API client, and performs the deployment via DeploymentRepository.create() or .createTenanted(). Returns deployment task details including URIs for monitoring.
async ({ spaceName, projectName, releaseVersion, environmentNames, tenants, tenantTags, forcePackageRedeployment, updateVariableSnapshot, forcePackageDownload, specificMachineNames, excludedMachineNames, skipStepNames, useGuidedFailure, runAt, noRunAfter, variables, deploymentFreezeOverrideReason, deploymentFreezeNames, confirm, }) => { try { // Validate environment names if (!environmentNames || environmentNames.length === 0) { throw new Error("At least one environment name must be provided."); } // Determine if this is a tenanted deployment const isTenanted = (tenants && tenants.length > 0) || (tenantTags && tenantTags.length > 0); // Validate tenanted deployment constraints if (isTenanted && environmentNames.length !== 1) { throw new Error( `Tenanted deployments can only target one environment at a time. You provided ${environmentNames.length} environments. ` + `For tenanted deployments, specify exactly one environment in environmentNames, then use tenants or tenantTags to target specific tenants.`, ); } const tenantSummary = isTenanted ? ` for tenants [${(tenants ?? []).join(", ")}${ tenantTags?.length ? `; tags: ${tenantTags.join(", ")}` : "" }]` : ""; const confirmMessage = `Deploy release ${releaseVersion} of ${projectName} to ` + `[${environmentNames.join(", ")}]${tenantSummary} in space ${spaceName}?`; // Build common parameters const commonParams = { spaceName: spaceName, ProjectName: projectName, ...(forcePackageRedeployment !== undefined && { ForcePackageRedeployment: forcePackageRedeployment, }), ...(updateVariableSnapshot !== undefined && { UpdateVariableSnapshot: updateVariableSnapshot, }), ...(forcePackageDownload !== undefined && { ForcePackageDownload: forcePackageDownload, }), ...(specificMachineNames && { SpecificMachineNames: specificMachineNames, }), ...(excludedMachineNames && { ExcludedMachineNames: excludedMachineNames, }), ...(skipStepNames && { SkipStepNames: skipStepNames }), ...(useGuidedFailure !== undefined && { UseGuidedFailure: useGuidedFailure, }), ...(runAt && { RunAt: new Date(runAt) }), ...(noRunAfter && { NoRunAfter: new Date(noRunAfter) }), ...(variables && { Variables: variables }), ...(deploymentFreezeOverrideReason && { DeploymentFreezeOverrideReason: deploymentFreezeOverrideReason, }), ...(deploymentFreezeNames && { DeploymentFreezeNames: deploymentFreezeNames, }), }; const tenantedCommand = { ...commonParams, ReleaseVersion: releaseVersion, EnvironmentName: environmentNames[0], Tenants: tenants || [], TenantTags: tenantTags || [], }; const untenantedCommand = { ...commonParams, ReleaseVersion: releaseVersion, EnvironmentNames: environmentNames, }; const confirmation = await requireConfirmation(server, { message: confirmMessage, fallbackConfirm: confirm, change: { source: {}, target: isTenanted ? tenantedCommand : untenantedCommand, }, }); if (!confirmation.confirmed) { return unconfirmedResponse(confirmation, { action: "deployment" }); } const configuration = getClientConfigurationFromEnvironment(); const client = await Client.create(configuration); const deploymentRepository = new DeploymentRepository( client, spaceName, ); const deploymentType = isTenanted ? "tenanted" : "untenanted"; const response = isTenanted ? await deploymentRepository.createTenanted(tenantedCommand) : await deploymentRepository.create(untenantedCommand); // Format the response const tasks = response.DeploymentServerTasks || []; const encodedSpace = encodeURIComponent(spaceName); return { content: [ { type: "text", text: JSON.stringify( { success: true, deploymentType, deploymentsCreated: tasks.length, deploymentTasks: tasks.map((task) => ({ taskId: task.ServerTaskId, deploymentId: task.DeploymentId, taskResourceUri: `octopus://spaces/${encodedSpace}/tasks/${encodeURIComponent(task.ServerTaskId)}/details`, })), message: `Successfully created ${tasks.length} deployment(s) for release ${releaseVersion}`, helpText: `Feed each deploymentTasks[].taskResourceUri into read_resource (or resources/read) to monitor deployment progress via the structured ActivityLogs tree. For the lighter metadata-only view, swap /details off the URI. To search the raw log for a specific error or step, call grep_task_log with the taskId. Use list_deployments for high-level deployment listings.`, }, null, 2, ), }, ], }; } catch (error) { // Handle validation errors from our code if (error instanceof Error && !error.message.includes("octopus")) { return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error.message, }, null, 2, ), }, ], isError: true, }; } // Handle Octopus API errors handleOctopusApiError(error, { entityType: "deployment", spaceName, helpText: "Use list_projects to find valid project names, list_environments for environment names, find_releases to verify the release exists, and find_tenants for tenant information. Ensure you have permissions to create deployments.", }); } }, ); } - src/tools/deployRelease.ts:25-102 (schema)Zod input schema defining all deploy_release parameters: spaceName, projectName, releaseVersion, environmentNames, tenants, tenantTags, forcePackageRedeployment, updateVariableSnapshot, forcePackageDownload, specificMachineNames, excludedMachineNames, skipStepNames, useGuidedFailure, runAt, noRunAfter, variables, deploymentFreezeOverrideReason, deploymentFreezeNames, and confirm.
inputSchema: { spaceName: z.string().describe("The space name"), projectName: z.string().describe("The project name"), releaseVersion: z .string() .describe("The release version to deploy (e.g., '1.0.0')"), environmentNames: z .array(z.string()) .describe( "Array of environment names. For tenanted deployments, must contain exactly one environment.", ), tenants: z .array(z.string()) .optional() .describe("Array of tenant names for tenanted deployment (optional)"), tenantTags: z .array(z.string()) .optional() .describe( "Array of tenant tags for tenanted deployment (e.g., ['Region/US-West', 'Tier/Production'])", ), forcePackageRedeployment: z .boolean() .optional() .describe("Force redeployment of packages"), updateVariableSnapshot: z .boolean() .optional() .describe("Update the variable snapshot"), forcePackageDownload: z .boolean() .optional() .describe("Force package download"), specificMachineNames: z .array(z.string()) .optional() .describe("Deploy to specific machines only"), excludedMachineNames: z .array(z.string()) .optional() .describe("Exclude specific machines from deployment"), skipStepNames: z .array(z.string()) .optional() .describe("Skip specific deployment steps"), useGuidedFailure: z .boolean() .optional() .describe("Use guided failure mode"), runAt: z .string() .optional() .describe("Schedule deployment for later (ISO 8601 date string)"), noRunAfter: z .string() .optional() .describe( "Don't run deployment after this time (ISO 8601 date string)", ), variables: z .record(z.string()) .optional() .describe("Prompted variable values as key-value pairs"), deploymentFreezeOverrideReason: z .string() .optional() .describe("Reason for overriding deployment freeze"), deploymentFreezeNames: z .array(z.string()) .optional() .describe("Names of deployment freezes to override"), confirm: z .boolean() .optional() .describe( "Required only when the MCP client does not support elicitation. Set to true to confirm deployment; otherwise the tool aborts.", ), }, - src/tools/deployRelease.ts:285-293 (registration)Registration of the deploy_release tool via registerToolDefinition(), setting toolset to 'deployments', readOnly to false, and minimumOctopusVersion to '2022.3.5512'.
registerToolDefinition({ toolName: "deploy_release", config: { toolset: "deployments", readOnly: false }, registerFn: registerDeployReleaseTool, // DeploymentRepository.create / .createTenanted use the Executions API // (~/api/{space}/deployments/create/v1 and /create/tenanted/v1-alpha) which // the api-client refuses to call against servers older than 2022.3.5512. minimumOctopusVersion: "2022.3.5512", }); - src/tools/deployRelease.ts:13-283 (handler)The registerDeployReleaseTool function that registers the tool with the MCP server, including the full input schema and handler callback.
export function registerDeployReleaseTool(server: McpServer) { server.registerTool( "deploy_release", { title: "Deploy a release to environments in Octopus Deploy", description: `Deploy a release to one or more environments in Octopus Deploy This tool supports both tenanted and untenanted deployments: - **Untenanted**: Don't provide tenants or tenantTags. Can deploy to multiple environments at once. - **Tenanted**: Provide tenants or tenantTags. Can only deploy to ONE environment, but can target multiple tenants. The tool automatically determines which deployment type to use based on the parameters provided.`, inputSchema: { spaceName: z.string().describe("The space name"), projectName: z.string().describe("The project name"), releaseVersion: z .string() .describe("The release version to deploy (e.g., '1.0.0')"), environmentNames: z .array(z.string()) .describe( "Array of environment names. For tenanted deployments, must contain exactly one environment.", ), tenants: z .array(z.string()) .optional() .describe("Array of tenant names for tenanted deployment (optional)"), tenantTags: z .array(z.string()) .optional() .describe( "Array of tenant tags for tenanted deployment (e.g., ['Region/US-West', 'Tier/Production'])", ), forcePackageRedeployment: z .boolean() .optional() .describe("Force redeployment of packages"), updateVariableSnapshot: z .boolean() .optional() .describe("Update the variable snapshot"), forcePackageDownload: z .boolean() .optional() .describe("Force package download"), specificMachineNames: z .array(z.string()) .optional() .describe("Deploy to specific machines only"), excludedMachineNames: z .array(z.string()) .optional() .describe("Exclude specific machines from deployment"), skipStepNames: z .array(z.string()) .optional() .describe("Skip specific deployment steps"), useGuidedFailure: z .boolean() .optional() .describe("Use guided failure mode"), runAt: z .string() .optional() .describe("Schedule deployment for later (ISO 8601 date string)"), noRunAfter: z .string() .optional() .describe( "Don't run deployment after this time (ISO 8601 date string)", ), variables: z .record(z.string()) .optional() .describe("Prompted variable values as key-value pairs"), deploymentFreezeOverrideReason: z .string() .optional() .describe("Reason for overriding deployment freeze"), deploymentFreezeNames: z .array(z.string()) .optional() .describe("Names of deployment freezes to override"), confirm: z .boolean() .optional() .describe( "Required only when the MCP client does not support elicitation. Set to true to confirm deployment; otherwise the tool aborts.", ), }, annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, }, async ({ spaceName, projectName, releaseVersion, environmentNames, tenants, tenantTags, forcePackageRedeployment, updateVariableSnapshot, forcePackageDownload, specificMachineNames, excludedMachineNames, skipStepNames, useGuidedFailure, runAt, noRunAfter, variables, deploymentFreezeOverrideReason, deploymentFreezeNames, confirm, }) => { try { // Validate environment names if (!environmentNames || environmentNames.length === 0) { throw new Error("At least one environment name must be provided."); } // Determine if this is a tenanted deployment const isTenanted = (tenants && tenants.length > 0) || (tenantTags && tenantTags.length > 0); // Validate tenanted deployment constraints if (isTenanted && environmentNames.length !== 1) { throw new Error( `Tenanted deployments can only target one environment at a time. You provided ${environmentNames.length} environments. ` + `For tenanted deployments, specify exactly one environment in environmentNames, then use tenants or tenantTags to target specific tenants.`, ); } const tenantSummary = isTenanted ? ` for tenants [${(tenants ?? []).join(", ")}${ tenantTags?.length ? `; tags: ${tenantTags.join(", ")}` : "" }]` : ""; const confirmMessage = `Deploy release ${releaseVersion} of ${projectName} to ` + `[${environmentNames.join(", ")}]${tenantSummary} in space ${spaceName}?`; // Build common parameters const commonParams = { spaceName: spaceName, ProjectName: projectName, ...(forcePackageRedeployment !== undefined && { ForcePackageRedeployment: forcePackageRedeployment, }), ...(updateVariableSnapshot !== undefined && { UpdateVariableSnapshot: updateVariableSnapshot, }), ...(forcePackageDownload !== undefined && { ForcePackageDownload: forcePackageDownload, }), ...(specificMachineNames && { SpecificMachineNames: specificMachineNames, }), ...(excludedMachineNames && { ExcludedMachineNames: excludedMachineNames, }), ...(skipStepNames && { SkipStepNames: skipStepNames }), ...(useGuidedFailure !== undefined && { UseGuidedFailure: useGuidedFailure, }), ...(runAt && { RunAt: new Date(runAt) }), ...(noRunAfter && { NoRunAfter: new Date(noRunAfter) }), ...(variables && { Variables: variables }), ...(deploymentFreezeOverrideReason && { DeploymentFreezeOverrideReason: deploymentFreezeOverrideReason, }), ...(deploymentFreezeNames && { DeploymentFreezeNames: deploymentFreezeNames, }), }; const tenantedCommand = { ...commonParams, ReleaseVersion: releaseVersion, EnvironmentName: environmentNames[0], Tenants: tenants || [], TenantTags: tenantTags || [], }; const untenantedCommand = { ...commonParams, ReleaseVersion: releaseVersion, EnvironmentNames: environmentNames, }; const confirmation = await requireConfirmation(server, { message: confirmMessage, fallbackConfirm: confirm, change: { source: {}, target: isTenanted ? tenantedCommand : untenantedCommand, }, }); if (!confirmation.confirmed) { return unconfirmedResponse(confirmation, { action: "deployment" }); } const configuration = getClientConfigurationFromEnvironment(); const client = await Client.create(configuration); const deploymentRepository = new DeploymentRepository( client, spaceName, ); const deploymentType = isTenanted ? "tenanted" : "untenanted"; const response = isTenanted ? await deploymentRepository.createTenanted(tenantedCommand) : await deploymentRepository.create(untenantedCommand); // Format the response const tasks = response.DeploymentServerTasks || []; const encodedSpace = encodeURIComponent(spaceName); return { content: [ { type: "text", text: JSON.stringify( { success: true, deploymentType, deploymentsCreated: tasks.length, deploymentTasks: tasks.map((task) => ({ taskId: task.ServerTaskId, deploymentId: task.DeploymentId, taskResourceUri: `octopus://spaces/${encodedSpace}/tasks/${encodeURIComponent(task.ServerTaskId)}/details`, })), message: `Successfully created ${tasks.length} deployment(s) for release ${releaseVersion}`, helpText: `Feed each deploymentTasks[].taskResourceUri into read_resource (or resources/read) to monitor deployment progress via the structured ActivityLogs tree. For the lighter metadata-only view, swap /details off the URI. To search the raw log for a specific error or step, call grep_task_log with the taskId. Use list_deployments for high-level deployment listings.`, }, null, 2, ), }, ], }; } catch (error) { // Handle validation errors from our code if (error instanceof Error && !error.message.includes("octopus")) { return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error.message, }, null, 2, ), }, ], isError: true, }; } // Handle Octopus API errors handleOctopusApiError(error, { entityType: "deployment", spaceName, helpText: "Use list_projects to find valid project names, list_environments for environment names, find_releases to verify the release exists, and find_tenants for tenant information. Ensure you have permissions to create deployments.", }); } }, ); } - Helper used by the handler to build the Octopus API client configuration from environment variables (OCTOPUS_SERVER_URL, OCTOPUS_API_KEY, OCTOPUS_ACCESS_TOKEN).
export function getClientConfigurationFromEnvironment(): ClientConfiguration { return getClientConfiguration({ instanceURL: env["CLI_SERVER_URL"] || env["OCTOPUS_SERVER_URL"], apiKey: env["OCTOPUS_API_KEY"], accessToken: env["OCTOPUS_ACCESS_TOKEN"], }); } - Helper used by the handler to gate destructive operations behind user confirmation, supporting both native MCP elicitation and a fallback confirm parameter.
export async function requireConfirmation( server: McpServer, opts: RequireConfirmationOptions, ): Promise<ConfirmationResult> { if (env["OCTOPUS_SKIP_ELICITATION"] === "true") { return { confirmed: true, reason: "envSkip" }; } const capabilities = server.server.getClientCapabilities(); if (capabilities?.elicitation) { const result = await server.server.elicitInput({ mode: "form", message: buildConfirmationMessage(opts.message, opts.change), // Empty properties → most clients render as a plain Accept/Decline prompt. requestedSchema: { type: "object", properties: {} }, }); switch (result.action) { case "accept": return { confirmed: true, reason: "accepted" }; case "decline": return { confirmed: false, reason: "declined" }; case "cancel": default: return { confirmed: false, reason: "cancelled" }; } } if (opts.fallbackConfirm === true) { return { confirmed: true, reason: "fallbackConfirm" }; } if (opts.fallbackConfirm === false) { return { confirmed: false, reason: "declined" }; } return { confirmed: false, reason: "confirmationRequired" }; } - src/helpers/errorHandling.ts:20-79 (helper)Helper used by the handler to format Octopus API errors with actionable messages for authentication, connection, and 404 issues.
export function handleOctopusApiError( error: unknown, context: { entityType?: string; entityId?: string; spaceName?: string; helpText?: string; }, ): never { const { entityType, entityId, spaceName, helpText } = context; // Handle 404/not found errors if ( isErrorWithMessage(error, "not found") || isErrorWithMessage(error, "404") ) { if (entityType && entityId && spaceName) { throw new Error( `${entityType.charAt(0).toUpperCase() + entityType.slice(1)} '${entityId}' not found in space '${spaceName}'. ` + (helpText || `Verify the ${entityType} ID is correct using list_${entityType}s.`), ); } if (spaceName) { throw new Error( `Space '${spaceName}' not found. Use list_spaces to see available spaces. Space names are case-sensitive.`, ); } } // Handle authentication errors if ( isErrorWithMessage(error, "authentication") || isErrorWithMessage(error, "401") || isErrorWithMessage( error, "You must be logged in to request this resource", ) || isErrorWithMessage(error, "provide a valid API key") ) { throw new Error( "Authentication failed. Ensure a valid API key (OCTOPUS_API_KEY) or access token (OCTOPUS_ACCESS_TOKEN) is provided. " + "You can generate an API key from your Octopus Deploy user profile.", ); } // Handle connection errors if ( isErrorWithMessage(error, "connect") || isErrorWithMessage(error, "timeout") ) { throw new Error( "Cannot connect to Octopus Deploy instance. Check that OCTOPUS_SERVER_URL environment variable is set correctly " + "(e.g., 'https://your-instance.octopus.app') and that the instance is accessible.", ); } // Re-throw the original error if no specific handling applies throw error; }