Repository navigation
Conversation
… on a ticket
The hosted server could not deploy: the tool lived in the local binary
alone. Now, behind the gateway crate's `deploy` feature, the MCP endpoint
serves imcp2-deploy's tools beside the core surface, through the one
handler both servers mount (`WithDeploy`, moved out of imcp2-local).
Three things the hosted shape needs that the local one did not:
How the bundle arrives. The agent's machine cannot hand this server a
file, and a tool call carries only what the model writes, so
`icp_deploy_upload` mints a one-time ticket inside the authenticated
call and returns the curl command that PUTs the archive to
`{mcp_path}/deploy/upload`. The ticket is 256 random bits, single use,
good for five minutes, bound to the session that minted it (the upload
it produces is deployable by that session alone), and sent in a header
so it stays out of proxy logs. The route is outside the bearer gate (the
ticket is its authentication) and streams the body to disk under a cap,
a declared length over it refused unread. `icp_deploy_bundle` takes the
upload id as `bundle_upload`; the archive goes with the deploy.
How the deploy call is answered. The endpoint answers a POST with plain
JSON, the one message a handler returns, and a deploy reports each phase
before its result. A `tools/call` of `icp_deploy_bundle` is therefore
dispatched to a second rmcp service configured for Server-Sent-Events
responses, still stateless; every other request keeps the JSON service.
A client that goes away. The event stream's body is wrapped so that its
drop cancels a token the deploy watches (`ClientGone`), and the deploy
stops at its next step, keeping its directory as the record.
Also: one deploy per session plus a server-wide cap (`concurrent`,
default 2 hosted, 1 local), refused past it rather than queued; deploy
directories and uploads under `{state_dir}/deploys`; the knobs
`MCP_DEPLOY_MAX_CONCURRENT` and `MCP_DEPLOY_MAX_UPLOAD_BYTES`; the native
build turns the feature on and CI tests it. The feature keeps `cargo
publish` of imcp2 from working until the icp CLI's crates are published.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 #242 (deploy-4). The hosted composition: the deploy tools on the MCP endpoint of the
imcp2binary, behind adeploycargo feature.What
WithDeploy<H>(crates/imcp2-deploy/src/server.rs): the handler both servers mount, moved out ofimcp2-local. It serves an inner handler's tools with the deploy tools beside them, the deploy skill next to the inner resources, and appends the DEPLOYING paragraph to the instructions, phrased from what the deploy tools know (how the bundle arrives, how many deploys run at once).LocalServernow wrapsWithDeploy<IcTools>and keeps only its login tools.src/deploy.rsin the gateway crate, under#[cfg(feature = "deploy")], withMcpServer::with_deploy(DeployConfig); the binary readsMCP_DEPLOY_MAX_CONCURRENT(default 2) andMCP_DEPLOY_MAX_UPLOAD_BYTES(default 64 MiB).How the bundle arrives
A tool call carries only what the model writes, and the agent's machine cannot hand this server a file. So:
icp_deploy_upload(listed only on a server with an upload store) mints a ticket inside the authenticated call and returns the upload URL, the ticket and the exactcurl -T … -H 'x-deploy-upload-ticket: …'line.PUT {mcp_path}/deploy/uploadsits outside the bearer gate (the ticket is its authentication), streams the body to disk under the cap, refuses a declared length over it unread, and answers201with theupload_id.icp_deploy_bundletakesbundle_upload; the archive is removed with the deploy.Ticket properties (
crates/imcp2-deploy/src/uploads.rs): 256 random bits; spent before a byte of the body is read; five-minute TTL; one pending ticket per session, minting another drops the previous; bound to the minting session, so the upload is deployable by that session alone; sent in a header, not the path, so it stays out of Caddy's access log. An upload waits fifteen minutes for its deploy; expired tickets and uploads are swept on each store access. Files are0600under a0700directory at{state_dir}/deploys/uploads.How the deploy call is answered
The endpoint answers a POST with plain JSON, the one message the handler returns; in rmcp's JSON mode a notification emitted before the result would be returned as the result. So the dispatch (
fallbackof the gated router) reads the JSON-RPC method and tool name off the buffered body and hands atools/calloficp_deploy_bundleto a secondStreamableHttpServiceconfigured for SSE responses, still stateless; everything else keeps the JSON service, so existing clients see no change. Both services share one handler factory and one upload store.A client that goes away
The SSE response body is wrapped (
cancel_on_drop) so that its drop, which is what a disconnect does on the server, cancels a token placed in the request's extensions asClientGone. The deploy tool ties that token to its stop (link_client_gone), for as long as the deploy runs. The deploy stops at its next step and keeps its directory as the record, as #242 made it do for a cancelled call.Concurrency
IcDeployTools::concurrent(n): a semaphore ofnslots across sessions (claimed before the session is even resolved, so a refusal costs nothing) plus one deploy per session. Past either the call is refused, not queued. Local keeps 1.Tests
uploads.rs: ticket lifecycle, session binding, caps declared and undeclared, nothing kept of a refusal, re-minting.src/deploy.rs: the whole loop over the composed router with a minted test token:tools/liststays JSON and lists the deploy tools;icp_deploy_uploadreturns a ticket; thePUTis refused without a ticket (401), on a wrong one (403), over the cap (413), accepted once (201) and spent; the deploy call is answered astext/event-streamcarrying the result, the upload is found for the session and spent by the deploy; an unauthenticated deploy call still gets the 401. Pluscancel_on_dropfires on body drop, and the dispatch predicate.cargo test --workspace(feature off) andcargo test -p imcp2 --features deployboth pass; clippy clean both ways. CI gets a step for the feature;deploy/native/build.shbuilds with it.Caveats
imcp2-deploypins icp-cli crates by git, which crates.io refuses, socargo publishofimcp2(andpublish-crate.yml's dry run) will fail while the optional dependency exists. That waits on the icp CLI's crates being published, as agreed.MemoryMax.read_body 30sapplies to the upload PUT; the native README says what that means for the cap.AuthStore::test_beareris the samecfg(test)helper Cap the MCP request body behind the bearer gate #241 adds; the hunk is identical, so the rebase once Cap the MCP request body behind the bearer gate #241 lands should be clean.🤖 Generated with Claude Code