Skip to content
Draft
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1666,13 +1666,16 @@ Security exemption execute workflow:

## MCP Prompts

Prompt modules are listed below. Some modules register additional aliases for external integrations (for example, `pipeline_error_analysis` for `debug-pipeline-failure`); aliases appear in the table with their canonical name noted.

### DevOps


| Prompt | Description | Parameters |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `build-deploy-app` | End-to-end CI/CD workflow: scan a git repo, generate CI pipeline (build & push Docker image), discover or generate K8s manifests, create CD pipeline, and deploy — with auto-retry on CI failures (up to 5 attempts) and CD failures (up to 3 attempts with user permission). On exhausted retries, provides Harness UI deep links to all created resources for manual investigation. | `repoUrl` (required), `imageName` (required), `projectId` (optional), `namespace` (optional) |
| `debug-pipeline-failure` | Analyze a failed execution: accepts an execution ID, pipeline ID, or Harness URL. Gets stage/step breakdown, failure details, delegate info, and failed step logs via `harness_diagnose`, then provides root cause analysis and suggested fixes. Automatically follows chained pipeline failures. | `executionId` (optional), `projectId` (optional) |
| `pipeline_error_analysis` | Alias for `debug-pipeline-failure` (used by ml-infra). Same behavior: analyze a failed execution via `harness_diagnose`, provide root cause analysis, and suggest fixes. Automatically follows chained pipeline failures. | `executionId` (optional), `projectId` (optional) |
| `pipeline_summarizer` | Fetch and summarize ALL step logs from a pipeline execution. Uses `harness_diagnose` with `include_logs: true, include_all_step_logs: true` to get every step's log, then presents a table with Step Name, Status, Duration, and What Happened (log-based summary). Does NOT skip any steps. | `executionId` (optional), `projectId` (optional) |
| `create-pipeline` | Generate a new pipeline YAML from natural language requirements, reviewing existing resources for context | `description` (required), `projectId` (optional) |
| `create-agent` | Interactively build a Harness AI agent — check existing agents, gather requirements, generate agent YAML spec using the agent-pipeline schema, confirm with user, then create or update via `harness_create`/`harness_update` | `agent_name` (required), `task_description` (required), `org_id` (optional), `project_id` (optional) |
Expand Down
66 changes: 40 additions & 26 deletions src/prompts/debug-pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import * as z from "zod/v4";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerDebugPipelinePrompt(server: McpServer): void {
server.registerPrompt(
"debug-pipeline-failure",
{
description: "Analyze a failed pipeline execution and suggest fixes. Accepts an execution ID, pipeline ID, or Harness URL.",
argsSchema: {
executionId: z.string().describe("The failed execution ID, pipeline ID, or a Harness URL").optional(),
projectId: z.string().describe("Project identifier").optional(),
},
},
async ({ executionId, projectId }) => {
// Detect if the input looks like a URL
const isUrl = executionId?.startsWith("http");
const idParam = isUrl
? `url="${executionId}"`
: `execution_id="${executionId}"`;

return {
messages: [{
role: "user" as const,
content: {
type: "text" as const,
text: `Analyze this failed Harness pipeline execution and provide:
const promptConfig = {
description: "Analyze a failed pipeline execution and suggest fixes. Accepts an execution ID, pipeline ID, or Harness URL.",
argsSchema: {
executionId: z.string().describe("The failed execution ID, pipeline ID, or a Harness URL").optional(),
projectId: z.string().describe("Project identifier").optional(),
},
};

function handleDebugPipeline({ executionId, projectId }: { executionId?: string; projectId?: string }) {
const isUrl = executionId?.startsWith("http");
const idParam = isUrl
? `url="${executionId}"`
: `execution_id="${executionId}"`;

return {
messages: [{
role: "user" as const,
content: {
type: "text" as const,
text: `Analyze this failed Harness pipeline execution and provide:

1. **Root cause** of the failure
2. **Which step failed** and why
Expand All @@ -43,9 +40,26 @@ Then analyze the diagnostic payload:
- Suggest the user either pass the missing values as \`inputs\` or use \`input_set_ids\` referencing a saved input set

Provide actionable recommendations based on the combined evidence.`,
},
}],
};
},
}],
};
}

export function registerDebugPipelinePrompt(server: McpServer): void {
server.registerPrompt(
"debug-pipeline-failure",
promptConfig,
async (args) => handleDebugPipeline(args),
);

// Alias: ml-infra requests this name via get_prompt("pipeline_error_analysis")
server.registerPrompt(
"pipeline_error_analysis",
{
...promptConfig,
description:
"Alias of debug-pipeline-failure — analyze a failed pipeline execution and suggest fixes. Accepts an execution ID, pipeline ID, or Harness URL.",
},
async (args) => handleDebugPipeline(args),
);
}
103 changes: 103 additions & 0 deletions tests/prompts/debug-pipeline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { registerDebugPipelinePrompt } from "../../src/prompts/debug-pipeline.js";

async function createTestClient(): Promise<Client> {
const server = new McpServer(
{ name: "test-server", version: "0.0.1" },
{ capabilities: { prompts: {} } },
);
registerDebugPipelinePrompt(server);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "0.0.1" });

await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);

return client;
}

describe("debug-pipeline-failure prompt", () => {
it("appears in the prompt list", async () => {
const client = await createTestClient();
const { prompts } = await client.listPrompts();

const prompt = prompts.find((p) => p.name === "debug-pipeline-failure");
expect(prompt).toBeDefined();
expect(prompt!.description).toContain("Analyze a failed pipeline execution");
});

it("registers pipeline_error_analysis as an alias with a distinguishing description", async () => {
const client = await createTestClient();
const { prompts } = await client.listPrompts();

const alias = prompts.find((p) => p.name === "pipeline_error_analysis");
expect(alias).toBeDefined();
expect(alias!.description).toContain("Alias of debug-pipeline-failure");
expect(alias!.description).toContain("analyze a failed pipeline execution");
});

it("pipeline_error_analysis returns the same prompt content", async () => {
const client = await createTestClient();

const original = await client.getPrompt({
name: "debug-pipeline-failure",
arguments: { executionId: "exec-123" },
});
const alias = await client.getPrompt({
name: "pipeline_error_analysis",
arguments: { executionId: "exec-123" },
});

const originalText = (original.messages[0].content as { type: string; text: string }).text;
const aliasText = (alias.messages[0].content as { type: string; text: string }).text;
expect(aliasText).toBe(originalText);
});

it("interpolates executionId and projectId", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "pipeline_error_analysis",
arguments: {
executionId: "exec-abc-123",
projectId: "my-project",
},
});

expect(result.messages).toHaveLength(1);
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('execution_id="exec-abc-123"');
expect(text).toContain('project_id="my-project"');
});

it("detects URL input and uses url param", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "pipeline_error_analysis",
arguments: {
executionId: "https://app.harness.io/ng/#/account/abc/pipelines/exec123",
},
});

const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('url="https://app.harness.io/ng/#/account/abc/pipelines/exec123"');
expect(text).not.toContain("execution_id=");
});

it("references harness_diagnose with include_logs", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "pipeline_error_analysis",
arguments: { executionId: "exec123" },
});

const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain("harness_diagnose");
expect(text).toContain("include_logs=true");
});
});
Loading