Skip to content

imcp2-deploy 3/3: serve icp_deploy_bundle and icp_cycles_balance from the local MCP server - #232

Open
raymondk wants to merge 5 commits into
deploy-2-pipelinefrom
deploy-3-mcp-tools
Open

raymondk wants to merge 5 commits into
deploy-2-pipelinefrom
deploy-3-mcp-tools

Conversation

@raymondk

@raymondk raymondk commented Nov 3, 2026 •

Copy link
Copy Markdown

Summary

Last of three stacked PRs bringing icp deploy to the local MCP server: the tools over the host (#230) and the pipeline (#231), and their wiring into imcp2-local.

icp_deploy_bundle takes a bundle (by absolute path, or inline as base64, bounded on its length before decoding), the environment name, and optionally the canister-id mapping an earlier deploy returned. It answers with the status, the mapping to pass back next time, the canisters created and a per-phase log, as structured content under a declared output schema. A failed run still reports every canister it created, so the mapping can be passed back to resume rather than repeat. It needs a session granted "Actions & questions", checked before the bundle is decoded.

Who deploys. Calls are signed as the user's CLI identity: their default Internet Identity account at https://cli.id.ai, the origin icp identity link web signs in at. That is the principal the icp CLI already deploys as on the user's machine, so canisters created here are the ones icp lists and controls, funded from the same cycles balance. It is deliberately not the management identity the canister lifecycle tools act as; the docs say a deployed canister is reached through those tools only after adding their principal as a controller. A per-app delegation lives five minutes and a deploy can run longer, so the agent signs through RefreshingIdentity, which swaps in a fresh delegation from the session store every ten seconds, for the same principal, until the deploy ends.

icp_cycles_balance reports that principal and its cycles-ledger balance, the check a deploy that will create canisters starts with. Core has a tool of that name in the protocol half the local server does not serve; this is the same logic on the served surface, and should be dropped if that half is ever served.

A skill for the agent. The server also serves skill://deploy-with-mcp next to core's bundled IC skills, only where the tool is. It walks through the loop from the agent's side: bundle the project with icp project bundle, call icp_deploy_bundle with the environment's .icp/data/mappings/<environment>.ids.json as canister_ids, write the returned mapping back into that file so the next deploy upgrades rather than creates. The server instructions point at it. Not every client reads resources, so icp_deploy_guide returns the same document as a tool result, as core serves its Candid guide both ways.

Policy note for reviewers. Core's server instructions say no tool creates, funds or deploys canisters. This tool creates canisters from the user's cycles-ledger balance (2T each by default). The local server's instructions call it out as the one exception; the wording in core is untouched and may want revisiting.

Related issues

Changes

  • crates/imcp2-deploy/src/lib.rs: DeployBundleArgs / DeployBundleOutput, argument validation (exactly one bundle source, bounded base64, mode, principals), the IcDeployTools router with the two tools, CLI_ORIGIN, the skill document, structured ok/error results.
  • crates/imcp2-deploy/src/identity.rs: RefreshingIdentity.
  • crates/imcp2-deploy/static/skills/deploy-with-mcp.md: the skill, in the registry's SKILL.md shape.
  • crates/imcp2-core: cycles_balance_of(agent, principal) split out of cycles_balance, and Identities::agent_as made public, so the deploy tools can read a balance as an identity other than the management one.
  • crates/imcp2-local: server.rs composes the deploy router (list/call/get) next to the login tools, serves the deploy skill through list_resources/read_resource ahead of core's, and adds a DEPLOYING paragraph to the instructions; main.rs wires it and documents the identity; README gains a "Deploying a project bundle" section; Cargo.toml takes the dependency.

Testing

  • cargo build --locked --workspace --all-targets
  • cargo test --locked --workspace --all-targets (75 + 8 + 10 + 214 + 35 + 21 + 1; 8 new in the crate, local server round-trips extended)
  • cargo fmt --all -- --check / cargo clippy --locked --workspace --all-targets (clean)
  • npm test --prefix monitoring/mcp-status (dashboard unchanged)

New here: an oversized inline bundle refused on its base64 length, the refreshing identity's swap and same-principal check, argument validation and the structured error shape, the tool schemas and annotations, and the skill's frontmatter and steps. The local server's round-trip test expects 14 tools and checks a signed-out icp_deploy_bundle call is refused before any file is read; its resource round-trip expects the deploy skill in the listing and reads it whole through a real client.

Not covered: the Internet Identity round trip that mints a delegation at cli.id.ai, and an on-chain deploy (see #231).

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

🤖 Generated with Claude Code

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

Concurrent deployments or retries after an interrupted response can create duplicate funded canisters; balance checks may use the wrong network, and revoked grants may continue signing.

6 open issues
What changed in this PR

Completes the three-part deployment integration by exposing bundle deployment and cycles-balance tools through the local MCP server, while keeping deployment dependencies out of the published core crates.

Changes:

  • Adds authenticated deployment tools with validated inputs, structured results, and refreshing signing identities.
  • Composes the tools and deployment skill into the local server, with expanded MCP round-trip tests.
  • Documents the bundle–deploy–save-mapping workflow and exposes reusable core balance helpers.
File Description
crates/​imcp2-local/​src/​server.rs Routes deployment tools, serves the skill, and extends integration tests.
crates/​imcp2-local/​src/​main.rs Constructs and wires the deployment tool group.
crates/​imcp2-local/​README.md Documents deployment inputs, identities, and mapping persistence.
crates/​imcp2-local/​Cargo.toml Adds the deployment crate dependency.
crates/​imcp2-deploy/​static/​skills/​deploy-with-mcp.md Adds agent guidance for bundling, deploying, and saving IDs.
crates/​imcp2-deploy/​src/​lib.rs Implements tool schemas, validation, authorization, and results.
crates/​imcp2-deploy/​src/​identity.rs Adds delegation refresh during long-running deployments.
crates/​imcp2-core/​src/​management.rs Extracts a balance helper for an explicit principal and agent.
crates/​imcp2-core/​src/​lib.rs Exports balance helpers and their result type.
crates/​imcp2-core/​src/​identities.rs Makes identity-specific agent construction public.
Cargo.lock Records the local server’s deployment dependency.

Review effort: Balanced

Comment thread crates/imcp2-deploy/src/identity.rs Resolved
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Comment thread crates/imcp2-deploy/src/identity.rs Resolved
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 3, 2026 20:34

Copilot AI left a comment

Copy link
Copy Markdown

Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Comment thread crates/imcp2-local/src/server.rs Resolved
Comment thread crates/imcp2-deploy/src/lib.rs Resolved
Comment thread crates/imcp2-local/src/server.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 3, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown

Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Ok(id) => id,
Err(e) => return Ok(err(e)),
};
match deploy_bundle(identity, request).await {

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.

Still open: the runner builds its own engine and store (icp-sync-plugin), so the limiter belongs upstream, and host_for's doc records the gap. Refusing sync steps until then would refuse the asset canister a bundle is deployed for. Left open until the runner has one.

Comment thread crates/imcp2-local/src/main.rs Resolved
@raymondk
raymondk marked this pull request as draft November 4, 2026 00:17
Comment on lines +79 to +85
/// Where the deploy's working directory is made (the system temporary
/// directory when `None`). A run that returns removes it; a run that is
/// cut off — the server stopped mid-deploy, the call cancelled — leaves
/// it, and with it `.icp/data/mappings/<environment>.ids.json`, which
/// records every canister the deploy created, so a caller who never got
/// the answer can recover the ids instead of creating a second set.
pub workdir: Option<Utf8PathBuf>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Any risk here of one user writing a malicious value to .icp/data/mappings/<environment>.ids.json that then is served to another user?

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.

For a given agent call, the server should create a temporary directory to run the deployment from and this file is a byproduct of the deployment. The directory is not supposed to be reused. even if it is reused the agent would still need a controller identity.

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.

No run reads another's file. Each call makes a fresh, randomly named directory (deploy_directory in deploy.rs, called where the scratch directory is made) under the user's local data directory (main.rs, deploys_in), extracts the bundle there, and that run alone reads it. The ids file in it is seeded from the call's own canister_ids argument and written by the library during the run; the bundle's own .icp/ entries are refused at extraction (host.rs), so a bundle cannot plant one. imcp2-local is one user per process, and the deploy tools are not on the gateway. What was missing: the directory's mode followed the umask, so another OS user could read a left-behind one where the home allows. 08516e7 makes it 0700 on Unix, with a test.

// records, and the build artifacts. Until the deploy starts it is a
// temporary directory, gone with any early return; from then on it is
// kept on purpose and removed only once the outcome — the ids above all —
// has been assembled, so a run that is cut off leaves its record behind.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same question as above, how do we make sure runs are isolated between users?

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.

Same answer as above: this is where the per-run directory is made, fresh and randomly named for every call, and a second deploy while one runs is refused before anything else (claim in lib.rs), so two runs never share one. As of 08516e7 it is created with mode 0700 on Unix.

/// The skill that teaches an agent the deploy loop around this tool — bundle
/// the project, call `icp_deploy_bundle` with the environment's canister-id
/// mapping, write the returned mapping back — served as the MCP resource
/// `skill://deploy-with-mcp` by the server that mounts the tool, next to

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Serving as MCP resource isn't reliable in all agent, see e.g. the candid resource as an example where we also exposed it through a tool for agents that don't work with skill resources.

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.

Done in 09ed508: icp_deploy_guide returns the same document as a tool result (text plus {content} structured output), the way core serves its Candid guide as both candid://reference and candid_syntax_guide. The resource stays; the server instructions and README name both.

raymondk and others added 3 commits November 11, 2026 21:17
The last of three changes bringing `icp deploy` to the local MCP server:
the tools over the bundle host and the deploy pipeline, and their wiring.

- `icp_deploy_bundle` takes a bundle (by absolute path, or inline as
  base64 bounded on its length before decoding), the environment name,
  and optionally the canister-id mapping an earlier deploy returned;
  it answers with the status, the mapping to pass back next time, the
  canisters created and a per-phase log, as structured content under a
  declared output schema. It needs a session granted "Actions &
  questions", checked before the bundle is decoded.
- Calls are signed as the user's CLI identity: their default Internet
  Identity account at `https://cli.id.ai`, the origin `icp identity
  link web` signs in at, so canisters created here are the ones `icp`
  lists and controls, funded from the same cycles balance. It is not the
  management identity the canister lifecycle tools act as; the docs say
  so. A per-app delegation lives five minutes and a deploy can run
  longer, so the agent signs through `RefreshingIdentity`, which swaps
  in a fresh delegation from the session store every ten seconds, for
  the same principal, until the deploy ends.
- `icp_cycles_balance` reports that principal and its cycles-ledger
  balance, the check a deploy that will create canisters starts with.
  Core's tool of that name sits in the protocol half the local server
  does not serve; this is the same logic on the served surface.
  `cycles_balance_of` is split out of core's `cycles_balance` and
  `Identities::agent_as` made public for it.
- The local server composes the deploy router next to the login tools,
  serves the `skill://deploy-with-mcp` skill (bundle, deploy, write the
  mapping back into `.icp/data/mappings/<environment>.ids.json`) ahead
  of core's bundled skills, and adds a DEPLOYING paragraph to its
  instructions. README and the crate docs describe the tool, the
  identity it acts as and the `ic`-only rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t past a cut-off

What the review found around the tools rather than in the pipeline, each
taken as it was raised.

A deploy signs as the account the first derivation resolved the session's
default to, and asks for that account by number from then on (core's
delegated_identity_resolved reports it): the default is the user's to
change, and a refresh that came back as another principal would have been
refused until the delegation in hand expired. When the store reports the
grant over — II's one terminal answer, RECONNECT_MSG, now public — the
identity stops signing at once instead of riding out that delegation.

Two deploys at once for one project, each with the mapping as it was
before either, would create two sets of canisters: a server runs one at a
time, and refuses rather than queues a second, since its mapping would be
stale by the time it ran. And a deploy's working directory is kept under
the OS's local data directory until the outcome is assembled, so a call
cut off mid-deploy leaves `.icp/data/mappings/<env>.ids.json` with every
canister it created where the README and the skill say to look.

The balance check reads the Internet Computer through the same mainnet
agent a deploy uses, whatever endpoint the rest of the server is pointed
at. `bundle_path` is trimmed and must be absolute before any I/O, and an
unparsable id is reported as what it is, not a valid principal. The deploy
skill joins the first page of resources alone.

The identity the tools act as is described as what it is: the default II
account at cli.id.ai, which `icp identity link web` links the CLI to —
the CLI's own principal only when its selected identity is that link.
The README, the skill, the server instructions and the module docs say
so, and point at `icp identity principal` for the comparison.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deploy cut off mid-run leaves the ids it created in its working
directory, under the local data directory the README points at. With
none reported by the OS the server fell back to the system temporary
directory in silence, where a cleanup may remove the record before the
user looks; it now fails at startup, naming what to set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
raymondk and others added 2 commits November 11, 2026 22:14
…esources

The skill around `icp_deploy_bundle` was served as the MCP resource
`skill://deploy-with-mcp` alone, and not every client reads resources
(review on #232). `icp_deploy_guide` returns the same document as a tool
result, text and structured content, as core serves its Candid guide both
ways. The server's instructions and README say so, and the round-trip
test calls it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every deploy runs in a fresh, randomly named directory under the user's
local data directory (or the system temporary directory), and nothing
reads it but that run. Its mode followed the umask, though, so another
user of the machine could read what the bundle holds and the ids the
deploy records wherever the parent allowed it. On Unix the directory is
now made with mode 0700. A test creates two and checks that they differ,
sit under the parent, and are the owner's alone (review on #232).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@raymondk
raymondk marked this pull request as ready for review November 12, 2026 12:57
Copilot AI balanced review requested due to automatic review settings November 12, 2026 12:57

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

The deployment guide overstates compatibility protection because the upstream check skips comparison when deployed Candid metadata is unreadable; qualify the guarantee and advise separate verification in that case.

2 open issues
3 resolved since last review

Review effort: Balanced

Comment on lines +125 to +128
Canisters named in `canister_ids` are upgraded with the Candid compatibility
check; an incompatible interface change fails the deploy before any code is
replaced. Pass `skip_candid_check: true` only when the user has accepted the
break. `mode` (default `auto`; `install`, `reinstall`, `upgrade`),

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