Skip to content

Report each deploy phase as it completes, and stop the deploy when the call is cancelled - #242

Draft
raymondk wants to merge 2 commits into
deploy-3-mcp-toolsfrom
deploy-4-streaming
Draft

raymondk wants to merge 2 commits into
deploy-3-mcp-toolsfrom
deploy-4-streaming

Conversation

@raymondk

Copy link
Copy Markdown

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_bundle stays 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::Watch is handed to deploy_bundle: a stop token and an optional lines sender. collect_log sends 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).
  • The run is raced against stop with a biased select!: 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 in created and canister_ids, like a failure does. The tool returns it as an error result, like a failure.
  • An interrupted deploy keeps its working directory (test: 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.
  • The tool forwards each line to the client mid-call: as a progress notification when the request carried a progress token, else as a logging message. MCP logging is deprecated (SEP-2577) and rmcp marks it so; it is used only as the fallback, under #[allow(deprecated)] with the reason. The local server declares the logging capability so a client expects it.
  • stop is the request context's cancellation token, which rmcp cancels on the client's notifications/cancelled. The hosted server will add the connection going away as a second source in a later PR.
  • Skill, tool description, server instructions and README say the call lasts as long as the deploy, to send a progress token, and what cancelling does.

Not here: the hosted composition, the bundle upload, concurrency across sessions. Those are the next PRs on this stack.

🤖 Generated with Claude Code

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cancellation can discard submitted creation results before their IDs are persisted, leaving untracked canisters and causing duplicates on retry.

6 open issues
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 interrupted outcome 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),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Co-authored-by: Arshavir Ter-Gabrielyan <arshavir.ter.gabrielyan@dfinity.org>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants