feat: Add find_interruptions tool and interruption resource#76
feat: Add find_interruptions tool and interruption resource#76egorpavlikhin merged 10 commits intomainfrom
Conversation
Task data now flows through three space-scoped resource templates instead
of three dedicated tools. Bulky activity logs and step timings only travel
when the agent explicitly fetches the URI, so per-call response weight
drops dramatically on the common deployment-investigation path.
URIs (registered into the existing release/dispatch registry):
- octopus://spaces/{spaceName}/tasks/{taskId} -> ServerTask metadata (JSON)
- octopus://spaces/{spaceName}/tasks/{taskId}/details -> ServerTaskDetails (JSON)
- octopus://spaces/{spaceName}/tasks/{taskId}/log -> raw activity log (text/plain)
BREAKING CHANGE: removes get_task_by_id, get_task_details, and get_task_raw.
Callers should switch to resources/read on the URI above (or the existing
read_resource tool backstop on clients without native resource support).
get_task_from_url is unchanged; its consolidation is tracked separately
under the from_url issue.
Discoverability:
- Adds a top-level instructions string on the McpServer so dynamic-discovery
clients learn the resourceUri pattern and the read_resource backstop at
initialize time, before any tool list is fetched.
- Tightens read_resource's tool description with concrete URI examples and
cross-references to the tools that emit URIs.
References on existing tools updated to point at resource URIs:
- get_deployment_from_url's nextSteps now returns taskResourceUri /
taskLogResourceUri instead of suggesting the deleted get_task_details.
- deploy_release's post-deploy helpText points at the task resource URI.
- get_task_from_url's error and description references updated.
- README and docs/working-with-urls.md workflows rewritten to dereference
URIs via resources/read or read_resource.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /log resource returns the entire activity log as plain text, which is fine for short tasks but expensive for long-running deployments. When the agent already knows what it is looking for (an error string, a step name, a regex), grep_task_log returns only matching lines instead. Parameter shape mirrors GNU grep so the schema is self-explanatory to any LLM that knows grep: pattern (regex by default; fixedString:true for literal text) caseInsensitive (-i) invertMatch (-v) fixedString (-F) beforeContext (-B) afterContext (-A) maxCount (-m) The response includes totalMatches across the whole log (so the caller sees the true count even when truncated), 1-indexed line numbers, optional before/after context arrays, and the fullLogResourceUri for fall-through to the resource when grep was too narrow. Implementation reuses SpaceServerTaskRepository.getRaw — no new HTTP code. Pure-function grepLines() exported for unit tests; 12 tests cover each flag, context-window edge cases, regex error handling, and totalMatches accounting under maxCount. Server-level instructions and README updated so dynamic-discovery clients discover the tool alongside the /log resource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al path
The octopus://spaces/{spaceName}/tasks/{taskId}/log resource is deliberately
removed. Reasoning:
- Activity logs can be multi-megabyte. Exposing them as an addressable
resource invites agents to fetch the entire body when grep_task_log
returns only the matching lines (and a totalMatches count) at a tiny
fraction of the context cost.
- The "one canonical way to fetch any given shape" rule from the proposed
architecture: a single primitive per use case is easier to discover
correctly than two paths with subtly different cost profiles.
- The /tasks/{id}/details resource already covers structured access (the
ActivityLogs[] tree with categories, timings, and embedded entries) for
callers that need programmatic traversal rather than text search.
Cross-references swept:
- get_deployment_from_url's nextSteps payload now returns a grepTaskLogHint
object (pre-filled tool name, spaceName, taskId) instead of a
taskLogResourceUri field. Integration test updated.
- getTaskFromUrl, deployRelease, README, docs/working-with-urls.md and the
server-level instructions all rewritten to point at grep_task_log for
log search and the /details URI for structured traversal.
- read_resource's description explicitly notes there is no /log URI and
redirects callers to grep_task_log.
- grepTaskLog response field renamed fullLogResourceUri -> taskDetailsResourceUri
(pointing at /details) since the previous URI no longer exists.
- task.test.ts replaces the /log descriptor test with two guard tests:
(1) no descriptor named "task-log" is registered, (2) dispatching a /log
URI returns null. Re-introducing the resource would fail both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server-level `instructions` string sent at MCP handshake now includes the configured Octopus server URL on its first line. Lets agents tell the user (or themselves, when reasoning about scope) which Octopus instance they are talking to without an extra round trip. Resolution mirrors getClientConfigurationFromEnvironment's precedence: --server-url flag wins, then OCTOPUS_SERVER_URL env var. If neither is set, the instructions show a placeholder pointing at the right configuration entry points instead of failing silently — the actual connection still fails at runServer time with the existing error path. Pulled the CLI_SERVER_URL assignment earlier in src/index.ts so the instructions are constructed after the resolved URL is known. Removed the previous duplicate assignment further down. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single shared helper that any write tool can call before performing a mutation. Negotiates against client capabilities and degrades gracefully: 1. OCTOPUS_SKIP_ELICITATION=true env var bypasses the gate (automation/CI). 2. Client advertises elicitation capability -> SDK emits elicitation/create and the helper resolves accept/decline/cancel from the user response. 3. Client without elicitation -> helper falls back to a confirm: boolean arg the tool surfaces in its own input schema. Returns a discriminated ConfirmationResult so callers can tell an explicit user "no" (declined / cancelled) apart from "the user was never asked" (confirmationRequired). Tools surface the latter as isError: true so the LLM stops and asks the user instead of treating it as a real cancellation. Wires the helper into create_release and deploy_release, and documents the pattern in CLAUDE.md alongside the existing MCP SDK guidance. Refs AIF-356. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e prose
The confirmationRequired / cancelled response shapes were duplicated
verbatim in createRelease and deployRelease, with only the action noun
("release creation" vs "deployment") differing. As more write tools get
gated, this would multiply.
Move the response shapes into requireConfirmation.ts as a single
unconfirmedResponse(result, { action }) builder. The builder picks the
right shape (isError: true for confirmationRequired, soft cancellation
for declined/cancelled) and capitalizes the action for the cancelled
message. Tools collapse to two lines:
if (!confirmation.confirmed) {
return unconfirmedResponse(confirmation, { action: "deployment" });
}
Adds 6 builder tests covering both branches and the capitalization rule.
Updates CLAUDE.md to show the simpler pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the Harness `approval_instance` parity gap (AIF-374). Octopus
interruptions (manual interventions, guided failures, deployment
approvals) are now first-class in the MCP server, matching how the
Harness server exposes pending approvals.
Tool modes (picked by which arg is supplied):
- interruptionId → slim summary for a single interruption
- assignedToMe → only interruptions the authenticated user can act on;
resolves /users/me (cached 1h per process) and filters
on CanTakeResponsibility / HasResponsibility /
ResponsibleUserId
- regarding → native server-side filter for a related entity ID
(e.g. ServerTasks-867 — same call the Octopus UI uses)
- (none) → list, optionally with pendingOnly (default: true) and
skip / take
Each slim summary includes resourceUri pointing at the new
`octopus://spaces/{spaceName}/interruptions/{id}` resource. Dereferencing
returns the full body — Form.Elements (control types, Markdown
instructions, button options like Abort / Proceed), Form.Values,
RelatedDocumentIds, and the responsibility flags — so an LLM can answer
"what's waiting for me" with the slim list and "what are my options" via
a follow-up resource read. The slim summary also exposes taskResourceUri
for the surrounding task and a portal publicUrl deep link.
Form values are deliberately stripped from the slim summary
(formElementNames is keys only) to keep payloads lean and avoid
incidental leakage of free-text notes; the full Form is available via
the resource for explicit drill-in.
Deviations from the Linear ticket:
- spaceName kept required to match every other find_* tool.
- projectId / environmentId filters dropped: the /interruptions endpoint
doesn't natively support them. `regarding` is the supported native
filter and replaces them.
- No InterruptionRepository in @octopusdeploy/api-client, so the tool
uses raw client.get with the standard URL template — same pattern as
findTenants.
Tests:
- 10 unit tests for interruptionSummary (incl. the actual array shape
for Form.Elements observed against a live Octopus, and explicit
no-form-values-leak guard)
- 4 unit tests for the interruption resource (full body returned, ID
format validated pre-network, 404 translation, dispatch URL-decodes
spaceName)
- 9 integration tests covering all four modes and error paths
…terruptions # Conflicts: # src/helpers/__tests__/requireConfirmation.test.ts # src/helpers/requireConfirmation.ts # src/tools/createRelease.ts # src/tools/deployRelease.ts
akirayamamoto
left a comment
There was a problem hiding this comment.
Reviewed by Codex.
Summary
This PR adds find_interruptions plus an interruption resource:
octopus://spaces/{spaceName}/interruptions/{interruptionId}. The shape mostly makes sense: list calls stay slim, detailed form data is behind the resource URI, and the returned task URI gives callers a clear path back to the surrounding task.
Approve from me. I do suggest fixing the assignedToMe paging behaviour before merging, because it affects the main "what is assigned to me?" workflow. The server-instructions update is smaller and can be handled in the same pass.
Suggested fixes
Fix assignedToMe paging before merge
File: src/tools/findInterruptions.ts, lines 213-234
assignedToMe is described as listing interruptions the authenticated user can act on, but the implementation fetches a single /interruptions page and then filters that page client-side. If the first server page contains no matching interruptions and a later page does, the tool returns an empty items array and silently misses actionable interruptions.
That affects the main usage shown on the PR: "find all interruptions assigned to me". Either use a server-side responsibility filter if Octopus exposes one, or page through the server result set until enough post-filtered results are collected, or until the server result set is exhausted.
Advertise interruption resources in server instructions
File: src/index.ts, lines 85-87
The PR registers octopus://spaces/{spaceName}/interruptions/{interruptionId}, but the MCP handshake instructions still list only release and task resource families. Resource-aware clients and agents that rely on the server instructions for URI discovery will not learn that interruption resources exist until after they happen to call find_interruptions.
Please add the interruption URI to the resource family list. The toolset list in the same instructions string should include interruptions too.
Verification
I retried the targeted tests after installing dependencies in the PR worktree:
npm test -- findInterruptions interruptionResult:
- 16 tests passed.
- 7 tests failed.
- 2 test files passed.
- 1 test file failed.
The failures were all live integration successful-path tests in src/tools/__tests__/findInterruptions.integration.test.ts. They failed with a 401 from Octopus:
The API key you provided was not valid.
The unit/resource tests passed:
src/tools/__tests__/findInterruptions.test.tssrc/resources/__tests__/interruption.test.ts
…urce
Two PR review fixes:
1) assignedToMe paging (correctness fix)
The previous implementation fetched a single /interruptions page and
post-filtered. If the first server page contained no interruptions
for the authenticated user, the tool returned an empty list and
silently missed actionable interruptions on later pages — exactly
the headline use case ("what's waiting for me").
Octopus has no responsibleUserId query parameter, so post-filtering
is the only correctness-preserving option. The handler now scans
the result set 100 records at a time up to a safety cap of 500
(almost never reachable under pendingOnly: true; surfaces a hint
in filteredAs when reached so the LLM knows to narrow the query).
Caller skip/take are applied to the post-filter set, since "skip
the first N matches" is what users actually mean. The wrapper now
exposes:
filteredAs.serverTotalScanned — unfiltered records inspected
filteredAs.serverTotalAvailable — unfiltered server total
filteredAs.scanComplete — true if exhausted
filteredAs.scanIncompleteHint — present only when capped
2) Server instructions (discovery fix)
The MCP handshake instructions advertised only release and task
resource families. Resource-aware clients that rely on the
instructions for URI discovery wouldn't learn about
octopus://spaces/{spaceName}/interruptions/{id} until they
happened to call find_interruptions. Added the family to the list
and added "interruptions" to the toolset list.
Tests (6 new, all using mocked api-client):
- "scans subsequent pages when the first page contains zero matches
(the original bug)" — canary test for the regression
- safety cap enforced; scanIncompleteHint surfaces when reached
- short page treated as exhaustion
- skip/take applied to post-filter set, not unfiltered
- empty result returns [], not error
- non-assignedToMe path still passes server pagination through
unchanged
Integration test updated to assert on the new filteredAs scan metadata
shape; full suite 103 passing locally (up from 97), live integration
against localhost:8065 all 9 pass.

Closes the Harness
approval_instanceparity gap (AIF-374). Octopus interruptions — manual interventions, guided failures, deployment approvals — are now first-class in the MCP server, matching how the Harness server exposes pending approvals.Stacked on top of
egorp/aif-356-require-confirmation(PR base).What's new
find_interruptionstool — four modes, picked by which argument you supply:interruptionIdassignedToMe/users/me(cached 1h per process) and filters onCanTakeResponsibility/HasResponsibility/ResponsibleUserId. The wrapper surfacesfilteredAs.userIdso consumers can cite which user "me" was.regardingServerTasks-867) — the same call the Octopus UI makes from the task pagependingOnly(default:true) plusskip/takeoctopus://spaces/{spaceName}/interruptions/{id}Resource — full body for drill-in. Each slim summary'sresourceUripoints here. Dereferencing returnsForm.Elements(control types, Markdown instructions, button options like Abort / Proceed),Form.Values,RelatedDocumentIds, and the responsibility flags. The slim summary also exposestaskResourceUrifor the surrounding task and a portalpublicUrldeep link.This split lets an LLM answer "what's waiting for me?" with the cheap slim list and "what are my options?" via a targeted resource read — no overfetching.
Why these shapes
formElementNamesis keys only). Keeps payloads lean and avoids incidental leakage of free-text notes / submitted approvals into list responses; the fullFormis available via the resource for explicit drill-in./users/mecached per-process with a 1h TTL — mirrorsspaceResolver. The acceptance criterion required this; the userId is also surfaced in the response so it's auditable.Deviations from the Linear ticket
spaceNamekept required, not optional. Every otherfind_*tool requires it; deviating would force a default-space resolver we don't have.projectId/environmentIdfilters dropped. The/interruptionsendpoint doesn't natively support them.regardingis the supported native filter and replaces them. Per direction during planning: only expose what the endpoint actually supports.InterruptionRepositoryexists in@octopusdeploy/api-client, so the tool uses rawclient.get<ResourceCollection<…>>(\"~/api/{spaceId}/interruptions{?…}\", …)— same pattern asfindTenants.One bug caught during build-out
Working from the spec alone, I assumed
Form.Elementswas aRecord<string, unknown>and usedObject.keys()to extract names. The actual API response (verified against a live Octopus onlocalhost:8065with a realManualIntervention) returns it as an array of{ Name, Control, IsValueRequired }. The original code would have silently emitted numeric indices (\"0\",\"1\",\"2\") forformElementNames. Fixed and pinned with a unit test that uses the real-worldParagraph/TextArea/SubmitButtonGroupshape.ID-format validation also runs before the space resolve now, so a malformed
interruptionIdfails fast without a network round-trip.Test plan
Verified locally; reviewers can re-run:
npm run buildnpm run lintnpx vitest run— 14 new unit tests pass (10 forinterruptionSummary, 4 for the resource — full body returned, ID format validated pre-network, 404 translation, dispatch URL-decodes spaceName)TEST_SPACE_NAME=Default npx vitest run src/tools/__tests__/findInterruptions.integration.test.tsagainst a live Octopus onlocalhost:8065— all 9 integration scenarios pass, including:pendingOnly: falsereturns ≥pendingOnly: trueskip/takeregardingfilter (native pass-through)assignedToMesurfacesfilteredAs.userIdand/users/mecache returns the same id on repeatinterruptionIdrejected pre-network withInvalid interruption ID formatnpm start -- --toolsets core,interruptions— confirm onlyfind_interruptions(pluscore) registers; with--toolsets core,projectsthe tool is filtered outfind_interruptions { spaceName: \"Default\", assignedToMe: true }find_interruptions { spaceName: \"Default\", regarding: \"ServerTasks-867\" }read_resource { uri: \"octopus://spaces/Default/interruptions/Interruptions-1\" }returns full FormOut of scope
take_responsibility,submit_interruption) — would needrequireConfirmation(which exists on the base branch) and are explicitly not part of AIF-374.octopus://…/interruptions/{id}/detailsURI — the interruption body is small enough that a single resource is sufficient. The pattern is there if a future need arises.