diff --git a/README.md b/README.md index 0ac4b6ab..17f0c000 100644 --- a/README.md +++ b/README.md @@ -1666,6 +1666,8 @@ 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 @@ -1673,6 +1675,7 @@ Security exemption execute workflow: | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `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) | diff --git a/src/prompts/debug-pipeline.ts b/src/prompts/debug-pipeline.ts index 2ccdd790..42680c46 100644 --- a/src/prompts/debug-pipeline.ts +++ b/src/prompts/debug-pipeline.ts @@ -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 @@ -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), ); } diff --git a/tests/prompts/debug-pipeline.test.ts b/tests/prompts/debug-pipeline.test.ts new file mode 100644 index 00000000..cb8c4858 --- /dev/null +++ b/tests/prompts/debug-pipeline.test.ts @@ -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 { + 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"); + }); +});