Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerTools } from "./tools/index.js";
import { registerResources } from "./resources/index.js";
import { registerPrompts } from "./prompts/index.js";
import { Command } from "commander";
import dotenv from "dotenv";
import { createToolsetConfig } from "./utils/parseConfig.js";
Expand Down Expand Up @@ -138,6 +139,7 @@ if (toolsetConfig.allowDeletes && toolsetConfig.readOnlyMode) {

registerTools(server, toolsetConfig);
registerResources(server, toolsetConfig);
registerPrompts(server);

if (options.listToolsByVersion) {
printToolVersionAnalysis();
Expand Down
88 changes: 88 additions & 0 deletions src/prompts/addReleaseNotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { z } from "zod";
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerAddReleaseNotesPrompt(server: McpServer) {
server.registerPrompt(
"add_release_notes",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this would be more predictable if we split the workflow into draft_release_notes and set_release_notes.

draft_release_notes should be a non-mutating prompt: take explicit source material, like PRs, commits, tickets, or a user-provided changelog, and return a draft for review.

set_release_notes should probably be a tool, not a prompt. Updating ReleaseNotes is a concrete Octopus write operation, so it benefits from typed inputs, validation, confirmation, tests, and deterministic behavior. It should update an existing release only and not fall back to creating one.

For creating releases, I’d use the existing create_release tool and recommend passing releaseNotes there when needed. That tool already supports release notes, so a separate “create release with notes” prompt would duplicate behavior.

This keeps the boundaries clearer:

  • draft notes: prompt, no side effects
  • set notes on an existing release: tool, write operation
  • create a release with notes: existing create_release tool

The current add_release_notes prompt mixes drafting, release selection, updating, and fallback creation in one flow. That makes the outcome harder to predict, especially when releaseVersion is omitted or the release is not found.

{
title: "Add release notes to an existing Octopus release",
description:
"Summarize a set of changes (typically PRs) into a Markdown changelog and write it to an Octopus release. Primary path updates ReleaseNotes on an existing release; falls back to creating a new release if none matches.",
argsSchema: {
spaceName: z.string().describe("The Octopus space name (required)"),
projectName: z.string().describe("The Octopus project name (required)"),
releaseVersion: z
.string()
.optional()
.describe(
"Release version to update (optional — omit to be guided through picking from the latest releases, or to fall back to creating a new release)",
),
},
},
({ spaceName, projectName, releaseVersion }) => {
const versionClause = releaseVersion
? ` version **${releaseVersion}**`
: "";

const text = `Add release notes to${versionClause ? ` release${versionClause} of` : ""} project **${projectName}** in space **${spaceName}**.

Work through these steps in order. Think step-by-step and check in with the user at each marked decision point.

1. **Gather the change set.** If the user has not already supplied the changes in the surrounding conversation, ask them now. Accept any of:
- a list of PR titles and/or numbers,
- commit messages,
- Jira/Linear ticket summaries,
- a free-form bullet list of changes the user types out.

Do not invent changes the user did not provide.

2. **Draft the changelog.** Summarize the change set into Markdown:
- One \`## {version}\` heading (use \`${releaseVersion ?? "the resolved release version"}\` once it's known).
- If the input clearly groups into categories (e.g. PR labels like \`feat\`/\`fix\`/\`chore\`), use \`### Features\`, \`### Fixes\`, \`### Chores\` sub-sections. Otherwise emit a flat bullet list.
- One bullet per change. Sentence-case, present tense, no trailing period. Plain English — no marketing language, no emoji unless the user asked.
- Bold project, environment, and entity names.
- Quote any log lines, code, or commands in fenced code blocks.

Show the draft to the user and **wait for explicit approval** before writing anything back to Octopus.

3. **Find the release.**
- Resolve \`projectName\` to a project ID with \`list_projects\`.
- Call \`find_releases\` with the resolved \`projectId\`${releaseVersion ? ` and \`searchByVersion: "${releaseVersion}"\`` : " (and `searchByVersion` if the user supplied a version)"}.
- If multiple matches come back, or no version was supplied, show the most recent few and ask the user to pick one. If the user says "latest", pick the highest-version assembled release.
- Confirm the resolved release ID and version back to the user before proceeding.

4. **Fetch the full release body.** The \`find_releases\` summary deliberately omits heavy fields. Call \`read_resource\` with the \`resourceUri\` from step 3 (form: \`octopus://spaces/${spaceName}/releases/{releaseId}\`) to get the full release object — including \`SpaceId\`, \`ProjectId\`, \`ChannelId\`, \`Version\`, \`SelectedPackages\`, \`SelectedGitResources\`, \`CustomFields\`, and the existing \`ReleaseNotes\`.

5. **Update via PUT round-trip.** Octopus's \`PUT /releases/{id}\` endpoint requires the **full** release body back — any field you omit gets wiped. So:
- Take the object from step 4 exactly as-is.
- Replace **only** the \`ReleaseNotes\` field with the approved changelog from step 2.
- Call the \`execute\` tool with \`method: "PUT"\`, \`path: "/api/{SpaceId}/releases/{Id}"\` (use the \`SpaceId\` and \`Id\` from the fetched body), and \`body\` set to the modified object.

The PUT lands in the write tier, so the MCP client will show a confirmation prompt — this is expected. After confirmation, verify the update by re-reading the release and showing the user that \`ReleaseNotes\` changed while \`SelectedPackages\`, \`ChannelId\`, and other fields stayed the same.

6. **Fallback: release does not exist.** If \`find_releases\` returns no matches, this is the secondary path:
- Tell the user the release was not found and ask whether to create a new one with the drafted notes.
- On approval, call \`create_release\` with \`spaceName\`, \`projectName\`, \`releaseVersion\` (ask if not supplied), and \`releaseNotes\` set to the approved changelog. Pass through \`channelName\`, \`gitRef\`, etc. if the user provides them; otherwise let Octopus apply defaults.
- Be explicit that this is a new release, not an edit to an existing one.

**Gotchas to flag to the user:**
- **Octostache expressions don't re-evaluate.** Variable expressions like \`#{Octopus.Release.Number}\` in release notes are resolved against the variable snapshot at the moment the release is first created. Notes written via PUT are stored literally and not re-evaluated. If the user's draft contains \`#{...}\`, warn them and offer to substitute resolved literal values or strip the expressions.
- **Config-as-Code projects.** \`ReleaseNotes\` is stored on the release server-side, not in the Git repo, so the round-trip works the same way as for database-backed projects.
- **Permissions.** The user needs \`ReleaseEdit\` on the project. A 401/403 from \`execute\` means the API key/account is missing that permission — surface the error directly rather than retrying.

Follow the Octopus Deploy writing guide throughout: concise, direct, plain English. Bold project/release/environment names. Avoid speculation.`;

return {
messages: [
{
role: "user",
content: {
type: "text",
text,
},
},
],
};
},
);
}
72 changes: 72 additions & 0 deletions src/prompts/diagnoseDeploymentFailure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { z } from "zod";
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerDiagnoseDeploymentFailurePrompt(server: McpServer) {
server.registerPrompt(
"diagnose_deployment_failure",
{
title: "Diagnose a failed deployment or runbook run",
description:
"Analyze and summarize a failed Octopus Deploy deployment or runbook run, calling out what went wrong and suggesting how to fix it.",
argsSchema: {
spaceName: z.string().describe("The Octopus space name (required)"),
projectName: z.string().describe("The Octopus project name (required)"),
releaseVersion: z
.string()
.optional()
.describe(
"Release version to diagnose (optional — omit for the latest failed deployment, or when diagnosing a runbook run)",
),
runbookName: z
.string()
.optional()
.describe(
"Runbook name to diagnose (optional — supply when diagnosing a failed runbook run rather than a deployment)",
),
},
},
({ spaceName, projectName, releaseVersion, runbookName }) => {
const isRunbook = Boolean(runbookName);
const subject = isRunbook ? "runbook run" : "deployment";

const qualifiers: string[] = [];
if (runbookName) qualifiers.push(`runbook **${runbookName}**`);
if (releaseVersion) qualifiers.push(`release **${releaseVersion}**`);
const qualifierClause =
qualifiers.length > 0 ? ` (${qualifiers.join(", ")})` : "";

const text = `Diagnose the most recent failed ${subject} for project **${projectName}** in space **${spaceName}**${qualifierClause}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What if the one I want to diagnose is not the most recently failed? For example, not the lastest runbook run? It could be an intermittent one.

It could be possible for a release as well but less likely.


Follow this diagnostic approach, thinking step-by-step:
1. Identify the exact step/target where failure occurred.
2. Extract and quote the specific error message(s) from the task log.
3. Determine the error category (configuration, connectivity, permissions, resource availability, etc.).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Leaving the range of category open (and not well defined) could lead to unpredictable results, therefore, I wonder if extracting a category here is useful. Maybe we are looking for a summary?

4. Provide actionable remediation steps based on the error type.
5. Note any cascading failures that resulted from the primary issue.

Suggested tools on this MCP server:
- \`${isRunbook ? "find_runbooks" : "find_releases"}\` and \`list_deployments\` to locate the failed task.
- \`get_task_from_url\` or \`get_deployment_from_url\` if the user supplies a URL.
- \`read_resource\` against \`octopus://spaces/{spaceName}/tasks/{taskId}/details\` for the structured activity tree (steps, targets, timing).
- \`grep_task_log\` to search the raw log with GNU-grep semantics — prefer this over reading whole logs.
- \`find_events\` for surrounding audit context (who triggered what, when).
- When a parent task references child sub-deployments (e.g. \`Deploy a Release\` steps), drill into the failed child task with the same tools to retrieve the actual error.

Remember: releases in Octopus snapshot the deployment process and variables at creation time. Changes made afterwards do not affect existing releases — fixes usually require creating a new release.

Follow the Octopus Deploy writing guide: be concise, direct, and use plain English. Bold step/project/entity names, quote log lines in code blocks, and avoid speculation about manually-cancelled tasks.`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Follow the Octopus Deploy writing guide" is this guide available in the MCP server? If not it could be dropped.


return {
messages: [
{
role: "user",
content: {
type: "text",
text,
},
},
],
};
},
);
}
8 changes: 8 additions & 0 deletions src/prompts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerDiagnoseDeploymentFailurePrompt } from "./diagnoseDeploymentFailure.js";
import { registerAddReleaseNotesPrompt } from "./addReleaseNotes.js";

export function registerPrompts(server: McpServer) {
registerDiagnoseDeploymentFailurePrompt(server);
registerAddReleaseNotesPrompt(server);
}