Repository navigation
Conversation
…e call is cancelled A deploy lasts from seconds to minutes, and until now the call said nothing until it ended. The pipeline now hands each log line to a watcher the moment it is made, and the tool delivers it to the client mid-call: as a progress notification when the request carried a progress token (which is also what keeps a client's per-call timeout from running out), else as a logging message. The local server declares the logging capability so a client expects the latter. The same watch carries a stop: cancelling the request drops the run at its next step (a canister call already in flight completes on the network), and the outcome reports `interrupted` with every canister the run had created, like a failure does. An interrupted deploy keeps its working directory, since whoever asked for the stop may be a client that went away and will never read the answer; the directory's mapping file remains the record, as for a run that is cut off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Cancellation can discard submitted creation results before their IDs are persisted, leaving untracked canisters and causing duplicates on retry.
6 open issues
Drain submitted creations and persist IDs before returning on cancellation · New Check cancellation between plugin calls and before successful cleanup · New Stream recovery diagnostics appended during finalization · New Cancel and join the notification forwarder when the client stops draining · New Send periodic progress during long-running deploy tasks · New Implement logging/setLevel and apply its level to fallback logs · New
What changed in this PR
Extends the deploy tools introduced in #232 with progress reporting and request cancellation, preparing a shared call shape for local and hosted servers while keeping deployment as one blocking call.
Changes:
- Adds streamed phase logs, cancellation handling, and an
interruptedoutcome that retains the working directory. - Forwards updates through MCP progress notifications, with logging as the fallback.
- Updates deployment guidance and adds streaming and interruption tests.
| File | Description |
|---|---|
crates/imcp2-local/src/server.rs |
Declares logging support and updates deployment instructions. |
crates/imcp2-local/README.md |
Documents progress, cancellation, and recovery behavior. |
crates/imcp2-deploy/static/skills/deploy-with-mcp.md |
Updates agent guidance for watching and interrupted deploys. |
crates/imcp2-deploy/src/lib.rs |
Connects request cancellation, forwards updates, and handles interrupted results. |
crates/imcp2-deploy/src/deploy.rs |
Adds Watch, streams logs, handles interruption, and adds tests. |
crates/imcp2-deploy/Cargo.toml |
Adds the cancellation-token dependency. |
Cargo.lock |
Records the deploy crate’s tokio-util dependency. |
Review effort: Max
| tokio::pin!(run); | ||
| tokio::select! { | ||
| biased; | ||
| _ = watch.stop.cancelled() => None, |
| tokio::select! { | ||
| biased; | ||
| _ = watch.stop.cancelled() => None, | ||
| result = &mut run => Some(result), |
Author
There was a problem hiding this comment.
It's fine to allow more syncs because those are idempotent and can be retried.
It's more important that we don't miss a canister creation.
Comment on lines
+390
to
+393
| outcome.log.push(format!( | ||
| "interrupted; the deploy directory '{}' is kept, with the ids it recorded", | ||
| workdir.display() | ||
| )); |
Comment on lines
+438
to
+440
| // The sender went with the run; the forwarder ends once it has | ||
| // delivered what is left. | ||
| let _ = forwarder.await; |
Comment on lines
+515
to
+516
| let mut progress = 0.0; | ||
| while let Some(line) = lines.recv().await { |
| // A running deploy reports each phase mid-call: as progress when the | ||
| // request carried a token, else as a logging message, which a client | ||
| // only expects from a server that declares it. | ||
| info.capabilities.logging = Some(Default::default()); |
aterga
reviewed
Nov 13, 2026
Co-authored-by: Arshavir Ter-Gabrielyan <arshavir.ter.gabrielyan@dfinity.org>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Stacked on #232 (deploy-3). Fourth step of the deploy stack, and the first of the work that brings the deploy tool to the hosted server: the call shape both servers will share.
What
icp_deploy_bundlestays one blocking call, but now reports each phase as it completes and stops when the call is cancelled.Why
A deploy lasts from seconds to minutes (canister creation, installs, a frontend's asset sync) and the call said nothing until it ended. On the hosted server a silent minutes-long call is also what trips a client's per-call timeout. Progress notifications fix both: the client sees each phase, and the MCP SDK clients reset their timeout on every progress notification.
How
deploy::Watchis handed todeploy_bundle: astoptoken and an optionallinessender.collect_logsends each log line the moment it is made, so the outcome's log and the streamed lines are the same lines in the same order (test:the_log_is_streamed_as_it_is_made).stopwith a biasedselect!: cancelling drops the run at its next await, which is the next canister call or plugin step. A canister call already in flight completes on the network. The outcome reports a new status,interrupted, with every canister created before the stop increatedandcanister_ids, like a failure does. The tool returns it as an error result, like a failure.an_interrupted_deploy_stops_and_keeps_its_directory): whoever asked for the stop may be a client that went away and will never read the answer, so the directory's mapping file stays as the record, as it does for a run that is cut off.#[allow(deprecated)]with the reason. The local server declares theloggingcapability so a client expects it.stopis the request context's cancellation token, which rmcp cancels on the client'snotifications/cancelled. The hosted server will add the connection going away as a second source in a later PR.Not here: the hosted composition, the bundle upload, concurrency across sessions. Those are the next PRs on this stack.
🤖 Generated with Claude Code