Find interruptions
find_interruptionsFind manual interventions, guided failures, and deployment approvals in Octopus Deploy. List by assignment, related entity, or fetch details by interruption ID.
Instructions
Find interruptions (manual interventions, guided failures, deployment approvals) in an Octopus Deploy space.
Interruptions are the Octopus surface equivalent to pending approvals: a deployment or runbook run pauses and waits for a human to take action. Use this tool to enumerate them or to look up a single one.
Modes (picked by which arguments you supply):
interruptionId → fetch the slim summary for that interruption.
assignedToMe → list interruptions the authenticated user can act on; resolves /users/me (cached per session).
regarding → list interruptions related to a specific entity (ServerTasks-…, Deployments-…). Native server-side filter.
(none) → list all interruptions, optionally filtered by pendingOnly (default: true) and skip/take.
Each summary includes:
resourceUri → octopus://spaces/{spaceName}/interruptions/{id} for the FULL body (form definition with Markdown instructions, button options, control types, and any already-submitted values). Dereference this when the user asks for details about a specific interruption.
taskResourceUri → octopus://spaces/{spaceName}/tasks/{taskId} for the surrounding deployment/runbook task.
publicUrl → Octopus portal deep link to take action.
formElementNames → just the field names (e.g. Instructions, Notes, Result). Field values are NOT in the slim summary; fetch resourceUri for those.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| spaceName | Yes | Space name. | |
| interruptionId | No | Fetch the slim summary for a single interruption by ID (e.g. Interruptions-1). Mutually exclusive with regarding/assignedToMe/pendingOnly. For the full body (form definition, instructions, button options, submitted values) dereference the returned resourceUri. | |
| pendingOnly | No | Return only unprocessed (pending) interruptions. Defaults to true. Ignored when interruptionId is set. | |
| assignedToMe | No | Limit to interruptions the authenticated user can act on (CanTakeResponsibility or HasResponsibility, or where the user is the explicit ResponsibleUserId). When true, /users/me is resolved (cached per session). Octopus has no responsibleUserId query parameter, so the tool pages through the server result set and post-filters; pages are scanned up to a safety cap (filteredAs.scanComplete signals whether the entire result set was inspected). totalResults reflects the post-filter count; the unfiltered server total is exposed under filteredAs. Ignored when interruptionId is set. | |
| regarding | No | Native server-side filter to interruptions related to a specific entity ID (e.g. ServerTasks-1234, Deployments-5678). Ignored when interruptionId is set. | |
| skip | No | Pagination offset. Ignored when interruptionId is set. | |
| take | No | Pagination page size. Ignored when interruptionId is set. |
Implementation Reference
- src/tools/findInterruptions.ts:159-300 (handler)The main handler function that executes the find_interruptions logic. Supports three modes: single interruption lookup by ID, assigned-to-me filtering (pages through results and post-filters by user), and standard list mode with pagination.
export async function findInterruptionsHandler(params: FindInterruptionsParams) { const { spaceName, interruptionId, pendingOnly = true, assignedToMe, regarding, skip, take, } = params; // Cheap pre-check: validate the ID format before any network round-trip, // so callers get a clear error without paying for the space resolve first. if (interruptionId) { validateEntityId(interruptionId, "interruption", ENTITY_PREFIXES.interruption); } const configuration = getClientConfigurationFromEnvironment(); const client = await Client.create(configuration); let spaceId: string; try { spaceId = await resolveSpaceId(client, spaceName); } catch (error) { handleOctopusApiError(error, { spaceName }); } // Single-id lookup mode. if (interruptionId) { try { const interruption = await client.get<InterruptionResource>( "~/api/{spaceId}/interruptions/{interruptionId}", { spaceId, interruptionId }, ); return { content: [ { type: "text" as const, text: JSON.stringify( interruptionSummary(interruption, spaceName, configuration.instanceURL), ), }, ], }; } catch (error) { handleOctopusApiError(error, { entityType: "interruption", entityId: interruptionId, spaceName, helpText: "Call find_interruptions without interruptionId to list valid IDs.", }); } } // assignedToMe: scan multiple server pages and post-filter, since Octopus // has no native "responsibleUserId" query param. Without paging, a first // server page that contains zero matches would cause us to return an empty // result and silently miss actionable interruptions on later pages. if (assignedToMe) { const user = await getCurrentUserCached(client); const currentUserId = user.Id; const scan = await scanAssignedInterruptions(client, spaceId, { pendingOnly, regarding, currentUserId, }); const start = skip ?? 0; const end = take !== undefined ? Math.min(start + take, scan.matched.length) : scan.matched.length; const sliced = scan.matched.slice(start, end); return { content: [ { type: "text" as const, text: JSON.stringify({ // Post-filter counts: the LLM's "totalResults" is the number of // interruptions actually assigned to the user, not the unfiltered // server total. The latter is surfaced under filteredAs for // transparency. totalResults: scan.matched.length, itemsPerPage: sliced.length, numberOfPages: 1, lastPageNumber: 0, filteredAs: { userId: currentUserId, serverTotalScanned: scan.serverScanned, serverTotalAvailable: scan.serverTotal, scanComplete: scan.scanComplete, ...(scan.scanComplete ? {} : { scanIncompleteHint: "Hit the safety cap before exhausting the server result set. " + "Narrow the query (e.g. set regarding to a specific task, or keep pendingOnly: true) " + "to ensure complete results.", }), }, items: sliced.map((interruption) => interruptionSummary(interruption, spaceName, configuration.instanceURL), ), }), }, ], }; } // List mode (no per-user filter): pass server pagination through directly. const response = await client.get<ResourceCollection<InterruptionResource>>( "~/api/{spaceId}/interruptions{?skip,take,pendingOnly,regarding}", { spaceId, skip, take, pendingOnly, regarding, }, ); return { content: [ { type: "text" as const, text: JSON.stringify({ totalResults: response.TotalResults, itemsPerPage: response.ItemsPerPage, numberOfPages: response.NumberOfPages, lastPageNumber: response.LastPageNumber, items: (response.Items ?? []).map((interruption) => interruptionSummary(interruption, spaceName, configuration.instanceURL), ), }), }, ], }; } - src/tools/findInterruptions.ts:85-93 (schema)FindInterruptionsParams interface defining the input parameters: spaceName, interruptionId, pendingOnly, assignedToMe, regarding, skip, take.
export interface FindInterruptionsParams { spaceName: string; interruptionId?: string; pendingOnly?: boolean; assignedToMe?: boolean; regarding?: string; skip?: number; take?: number; } - Zod input schema (findInterruptionsInputSchema) and its refinement (findInterruptionsValidationSchema) that validates inputs and enforces mutual exclusivity between interruptionId and list filter parameters.
export const findInterruptionsInputSchema = z.object(findInterruptionsRawShape); export const findInterruptionsValidationSchema = findInterruptionsInputSchema.superRefine((args, ctx) => { if (!args.interruptionId) return; const conflicting: Array<keyof typeof args> = [ "regarding", "assignedToMe", "skip", "take", ]; for (const key of conflicting) { if (args[key] !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Provide either interruptionId (to fetch a single interruption) or list filters " + `(${conflicting.join(", ")}), not both.`, path: [key], }); } } }); - src/tools/findInterruptions.ts:371-406 (registration)Registration function (registerFindInterruptionsTool) that registers the tool with the MCP server, and the registerToolDefinition call that wires it into the tool registry with toolset 'interruptions' and readOnly: true.
export function registerFindInterruptionsTool(server: McpServer) { server.registerTool( "find_interruptions", { title: "Find interruptions", description: `Find interruptions (manual interventions, guided failures, deployment approvals) in an Octopus Deploy space. Interruptions are the Octopus surface equivalent to pending approvals: a deployment or runbook run pauses and waits for a human to take action. Use this tool to enumerate them or to look up a single one. Modes (picked by which arguments you supply): - interruptionId → fetch the slim summary for that interruption. - assignedToMe → list interruptions the authenticated user can act on; resolves /users/me (cached per session). - regarding → list interruptions related to a specific entity (ServerTasks-…, Deployments-…). Native server-side filter. - (none) → list all interruptions, optionally filtered by pendingOnly (default: true) and skip/take. Each summary includes: - resourceUri → octopus://spaces/{spaceName}/interruptions/{id} for the FULL body (form definition with Markdown instructions, button options, control types, and any already-submitted values). Dereference this when the user asks for details about a specific interruption. - taskResourceUri → octopus://spaces/{spaceName}/tasks/{taskId} for the surrounding deployment/runbook task. - publicUrl → Octopus portal deep link to take action. - formElementNames → just the field names (e.g. Instructions, Notes, Result). Field values are NOT in the slim summary; fetch resourceUri for those.`, inputSchema: findInterruptionsInputSchema, annotations: { readOnlyHint: true }, }, async (args) => { const parsed = findInterruptionsValidationSchema.safeParse(args); if (!parsed.success) return zodErrorResponse(parsed.error); return findInterruptionsHandler(parsed.data); }, ); } registerToolDefinition({ toolName: "find_interruptions", config: { toolset: "interruptions", readOnly: true }, registerFn: registerFindInterruptionsTool, }); - src/tools/findInterruptions.ts:46-83 (helper)interruptionSummary helper that transforms an InterruptionResource into the slim summary format (id, title, type, taskId, responsible info, resource URIs, public URL).
export function interruptionSummary( interruption: InterruptionResource, spaceName: string, instanceURL: string, ) { const encodedSpace = encodeURIComponent(spaceName); const encodedInterruptionId = encodeURIComponent(interruption.Id); const encodedTaskId = encodeURIComponent(interruption.TaskId); const elements = interruption.Form?.Elements ?? []; return { id: interruption.Id, title: interruption.Title, type: interruption.Type, taskId: interruption.TaskId, correlationId: interruption.CorrelationId, isPending: interruption.IsPending, isLinkedToOtherInterruption: interruption.IsLinkedToOtherInterruption, created: interruption.Created, relatedDocumentIds: interruption.RelatedDocumentIds ?? [], responsible: { teamIds: interruption.ResponsibleTeamIds ?? [], userId: interruption.ResponsibleUserId, canTakeResponsibility: interruption.CanTakeResponsibility, hasResponsibility: interruption.HasResponsibility, }, formElementNames: elements.map((element) => element.Name), resourceUri: `octopus://spaces/${encodedSpace}/interruptions/${encodedInterruptionId}`, taskResourceUri: `octopus://spaces/${encodedSpace}/tasks/${encodedTaskId}`, publicUrl: getPublicUrl(`${instanceURL}/app#/{spaceId}/tasks/{taskId}`, { spaceId: interruption.SpaceId, taskId: interruption.TaskId, }), publicUrlInstruction: "View and respond to this interruption in the Octopus Deploy web portal at the provided publicUrl. " + "For full form details (Markdown instructions, button options, control types) dereference the resourceUri.", }; } - scanAssignedInterruptions helper that pages through the server result set (up to 500 records) collecting interruptions that match the current user (by CanTakeResponsibility, HasResponsibility, or explicit ResponsibleUserId), since Octopus has no native responsibleUserId query param.
/** * Page through the unfiltered /interruptions result set, collecting items * the authenticated user can act on. Octopus has no responsibleUserId query * parameter, so post-filtering is the only correctness-preserving option. * * Safety cap: ASSIGNED_SCAN_MAX records inspected. Almost always hit only * with pendingOnly: false on busy spaces with deep history; the assigned * set under pendingOnly: true is virtually always small. */ export const ASSIGNED_SCAN_PAGE_SIZE = 100; export const ASSIGNED_SCAN_MAX = 500; interface AssignedScanResult { matched: InterruptionResource[]; serverTotal: number; serverScanned: number; scanComplete: boolean; } async function scanAssignedInterruptions( client: Client, spaceId: string, params: { pendingOnly: boolean; regarding?: string; currentUserId: string }, ): Promise<AssignedScanResult> { const matched: InterruptionResource[] = []; let serverScanned = 0; let serverTotal = 0; let scanComplete = false; while (serverScanned < ASSIGNED_SCAN_MAX) { const remaining = ASSIGNED_SCAN_MAX - serverScanned; const pageTake = Math.min(ASSIGNED_SCAN_PAGE_SIZE, remaining); const page = await client.get<ResourceCollection<InterruptionResource>>( "~/api/{spaceId}/interruptions{?skip,take,pendingOnly,regarding}", { spaceId, skip: serverScanned, take: pageTake, pendingOnly: params.pendingOnly, regarding: params.regarding, }, ); serverTotal = page.TotalResults; const items = page.Items ?? []; for (const interruption of items) { if ( interruption.CanTakeResponsibility || interruption.HasResponsibility || interruption.ResponsibleUserId === params.currentUserId ) { matched.push(interruption); } } serverScanned += items.length; // Empty page (or short page) means the server has no more. if (items.length === 0 || items.length < pageTake || serverScanned >= serverTotal) { scanComplete = true; break; } } return { matched, serverTotal, serverScanned, scanComplete }; }