You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two pieces of the local-MCP install story, both riding dist's custom-job seam in the release pipeline:
Every imcp2-local release now ships imcp2-local.mcpb — download, double-click, and Claude Desktop installs and manages the server. No PATH, no setup. setup --print had been promising this "once released".
The release notes explain the install scripts before the command, and tell you what to do after it; the crate README documents a verified install beside the one-liner.
The bundle
One bundle covers every Claude Desktop platform. MCPB's platform_overrides key on the OS alone, not the CPU, so a single bundle cannot choose between the Apple Silicon and Intel builds. It carries a universal macOS binary instead — the two release builds joined with lipo — beside the Windows binary. dist has no universal-binary support, so this is the one genuinely new build step.
Each slice is byte-identical to its attested archive's binary: the build script extracts each slice with lipo -thin, compares it with cmp, and stops on any difference. This is what keeps the arm64 slice's ad-hoc signature valid, and Apple Silicon won't execute arm64 code without one.
Every input archive must carry the release workflow's attestation for that very tag before it is used. The job goes on to attest the bundle, so a swapped input would otherwise come out wearing a fresh, legitimate attestation.
Building and publishing are separate jobs, split by privilege. Building runs code this repository didn't write (the MCPB CLI and its npm dependencies), so that job holds only a read-only token and no OIDC. A second job, which runs nothing but pinned actions and gh, attests the bundle and attaches it to the release.
Claude Desktop appends .exe itself for binary servers, so the manifest names the binary once, with no Windows override.
The manifest's tool list is read off the shipped binary's own tools/list, using the server's display titles, so the install dialog can't advertise a surface the server lacks:
Sign in with Internet Identity · Check sign-in status · Open an app (resolve origin + discover canisters) · Query a canister (Candid method or OQL) · Make a canister update call · Get Candid interface · Get a canister's API documentation · Get the OQL schema · Get the OQL query guide · Get the textual Candid guide · Get your principal at an app · List your accounts at an app
Fixed manifest fields live in crates/imcp2-local/mcpb/, with the repository's ICP logo (512×512, the size Claude Desktop recommends) and the published privacy policy, which Anthropic's directory requires of any listing. Display name and keywords follow the test bundle you built by hand earlier; adjust freely.
It is not code-signed yet, and the README says what that means: an unverified-developer warning; possibly a Gatekeeper refusal on the server's first macOS launch, since a browser download is quarantined and the binary isn't notarized; and an outright block in organizations that limit Claude Desktop to directory-listed extensions, which is what your own install attempt hit. Signing stays blocked on the organization's Developer ID credentials.
The release-note summary
The summary is prepended to every future release's notes, from .github/release-notes/imcp2-local-install-note.md. What it says is what I found by reading the shipped installers:
Both install into ~/.cargo/bin with an auto-updater and put that directory on PATH. The shell script appends to every shell profile it finds; the PowerShell one edits the Path registry key.
Where sha256sum is missing (older macOS releases, for one), the shell script prints a one-line notice and installs without checking its checksum. The PowerShell installer checks none.
What establishes provenance is the archive's attestation, checked against the release's own tag as the README's verified install does.
It then points at imcp2-local setup / --print / --remove, and at the bundle for Claude Desktop users.
Related issues
Follows the 0.5.0 release, the first imcp2-local binary release.
Changes
.github/scripts/build-mcpb.sh assembles the bundle from the release's own published archives.
Each archive is checked against its .sha256, falling back to shasum where sha256sum is missing so it doesn't repeat the installer's gap.
Each must also pass gh attestation verify --signer-workflow …/imcp2-local-release.yml --source-ref refs/tags/<tag> --deny-self-hosted-runners. That check is mandatory in CI; MCPB_ALLOW_UNATTESTED=1 opts out locally only and fails the build under CI.
After lipo -create, each slice is cmp-checked against its attested input.
After the last fetch, the script drops GH_TOKEN, GITHUB_TOKEN and the OIDC request variables from its environment.
It then renders the manifest and runs mcpb validate + pack, using the CLI installed from the lockfile below.
.github/scripts/mcpb-cli/package.json + package-lock.json pin the MCPB CLI's whole dependency tree: 55 packages, every one with an integrity hash and resolved from registry.npmjs.org, none with an install script. The script installs them with npm ci --ignore-scripts into its scratch directory and invokes the local CLI, with no npx.
.github/workflows/imcp2-local-mcpb.yml defines two jobs:
build (macos-15, contents: read + attestations: read, no OIDC) runs the script. macOS provides the native lipo, and the job executes the binary it just built to read the tool list.
publish (ubuntu-24.04, write + id-token + attestations) receives the bundle as an artifact, attests it as the new artifact it is, and attaches it to the release.
.github/workflows/imcp2-local-install-note.yml + .github/release-notes/imcp2-local-install-note.md — the notes job (ubuntu-24.04), made idempotent by a marker line that is matched in the shell rather than through a pipe, so the check stays reliable under pipefail.
dist-workspace.toml — both jobs under post-announce-jobs, plus github-custom-job-permissions granting the bundle workflow id-token/attestations beyond the default contents: write. dist puts that union on the caller job, and each of the bundle's two jobs takes only its share.
.github/workflows/imcp2-local-release.yml — regenerated with dist 0.31.0, never hand-edited.
crates/imcp2-local/README.md — the Install section, in order:
The verified path first. It resolves the newest imcp2-local-v* tag by paginating the releases API. (releases/latest is unusable here: production deploys publish release-* releases in this repo.) The steps are &&-chained with curl -f, so a failed attestation stops the install.
The installer one-liner, with its caveats.
The Claude Desktop bundle.
The source build.
Every verification command the README documents, for the archives and the bundle alike, is pinned to the release's tag with --source-ref and to GitHub-hosted runners. Archive names repeat across releases, so an older release's attested file would otherwise pass for a newer one's.
crates/imcp2-local/src/setup.rs — setup --print points at the shipped bundle.
docs/scoping-local-deployment.md — describes the single-bundle design, replacing one artifact per platform, together with its privilege-split assembly and the tag-pinned verification.
Testing
Built a real bundle locally from the published 0.5.0 archives with llvm-lipo:
mcpb validate passes the schema.
The zip entries are exactly icon.png, manifest.json, server/imcp2-local (mode 0755, x86_64 arm64) and server/imcp2-local.exe (PE32+ x86-64).
The manifest lists 12 tools.
arm64 slice carries LC_CODE_SIGNATURE and is byte-identical to the attested release binary.
Slice check: the build passes with a faithful lipo. A lipo wrapper that alters the thinned x86_64 slice stops the build with no bundle written.
Input provenance gate, run with a real gh against the published 0.5.0 archives:
All five archives pass the tag-bound check.
An archive with one byte appended fails, since there is no attestation for its digest.
A genuine archive under the wrong tag fails on the ref.
Opt-out matrix:
The opt-out builds with zero gh calls.
It is refused under CI.
A missing gh refuses.
A normal build verifies all four inputs against the tag.
Credential scrub, tested with shims around python3 (which launches the binary), npm and node:
None of them saw GH_TOKEN, GITHUB_TOKEN or the two OIDC variables.
All four attestation checks ran before the scrub.
Locked CLI: a bundle built from the lockfile has extracted contents identical to the npx-built one's.
The README's verified-install block, extracted verbatim and run under set -euo pipefail in a sandboxed HOME, resolves imcp2-local-v0.5.0, verifies it and installs it. The installed binary reports 0.5.0.
dist generate --check clean; all three workflow files parse as YAML; the generated caller job carries the three permissions.
The notes job's script, extracted from the workflow and run against the real 0.5.0 body with a stubbed gh:
It prepends the note and leaves dist's body intact.
A re-run is a no-op.
The marker check still finds the marker on a 2 MB body. The earlier printf | grep -q missed it 20 of 20 times under pipefail.
Tag selector run against the live release list: picks imcp2-local-v0.5.0 past v0.5.0 and release-2026-08-18-0.5.0.
cargo fmt --all --check, cargo test --workspace --all-targets (297 passed); clippy's remaining map_or warnings are pre-existing in imcp2-core.
Not testable here: installing in Claude Desktop itself (no macOS GUI), the jobs' first real run (including the artifact hand-off between the bundle's two jobs), and the bundle's own tag-bound verification command. All of these need the next imcp2-local-v* tag. The bundle workflow is the part most worth watching on that release.
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.
…fied install
The notes led with `curl … | sh` and said nothing about what it does. It
installs into ~/.cargo/bin, adds an auto-updater, and appends a PATH line
to every shell profile it can find; its checksum is baked into the script
and skipped silently on stock macOS, which has no sha256sum, while the
PowerShell installer checks none at all. For a binary that acts as the
user's Internet Identity, that is worth saying before the command.
dist writes the release body itself and has no setting for extra prose, so
this rides its documented custom-job seam: post-announce-jobs invokes
.github/workflows/imcp2-local-install-note.yml after the release is
published, and that job prepends
.github/release-notes/imcp2-local-install-note.md to the notes. Keeping the
prose in a markdown file means it is reviewed as rendered markdown rather
than as a string inside YAML — the first attempt embedded it in a heredoc,
which broke the block scalar and would have made the workflow unparseable.
A marker line makes a re-run a no-op. The release workflow was regenerated
with dist 0.31.0 rather than hand-edited, so it stays reproducible from
config.
The crate README gains the verified path it should be recommending —
download, `gh attestation verify --signer-workflow`, extract — beside the
one-liner carrying the same caveats, with version-agnostic
releases/latest/download URLs. It also no longer claims that no release has
been cut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…steps
`releases/latest` is the repository's latest release, and deploy-release.yml
publishes a GitHub Release for every `release-*` production deploy
(deploy-release.yml:249), so the next deploy would have made those URLs 404.
Confirmed against the live release list, which already interleaves `v*` and
`release-*` entries with this crate's. The verified path now resolves the
newest `imcp2-local-v*` tag with `gh release list` — no new dependency,
since that path already needs `gh` for the attestation — and the one-liner
is pinned with a note to substitute the current tag.
The verified path also creates `~/.local/bin` before installing into it and
says it must be on PATH, rather than promising a PATH change it never made.
Both the README and the release note stated the PowerShell installer checks
no checksum and then spoke of "either hash" being carried by a script piped
to a shell, which is not the Windows story at all. The caveat is now
singular to the shell script, with the attestation named as what establishes
provenance on both platforms.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
`gh release list` returns 30 releases by default, and this repository
publishes one per production deploy, so the newest `imcp2-local-v*` will
eventually page out and leave TAG empty. The lookup now paginates the
releases API and takes the first matching tag, which is stable however many
deploy releases accumulate.
The steps were also separate lines, so a failed attestation would print its
error and the install would proceed anyway — precisely the outcome the
verified path exists to prevent. They are now `&&`-chained, and the download
uses `curl -f` so an empty or wrong tag fails there rather than saving
GitHub's 404 body as the archive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
The shell installer emits a "skipping" message when sha256sum is unavailable, so calling the skipped check silent is inaccurate. State that it warns but continues successfully without verification.
Document visible warning and continued installation without verification
crates/imcp2-local/README.md:58
The shell installer does not skip this silently: its no-sha256sum path emits a "skipping" message and then returns success. Since this section is intended to explain the installer precisely, describe the visible warning and the fact that installation continues without verification.
Align PR description with pinned package-specific release URLs
crates/imcp2-local/README.md:66
The PR description says the README uses releases/latest/download URLs that do not rot, but this changed command is pinned to imcp2-local-v0.5.0 and tells readers to substitute it. Please update the description to match the implemented package-specific tag/pinned-release strategy; otherwise it describes behavior this change does not provide.
Installing leaves you with a binary on PATH and, from the release page
alone, no stated next step: dist's body is the install one-liners, the
download table and the attestation section, and nothing there says the
server still has to be registered with a client. The note now names
`setup`, `--print` and `--remove`, says to restart the client, and links
the README's per-client table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Every imcp2-local release now carries `imcp2-local.mcpb`: download it,
double-click it, and Claude Desktop installs and manages the server — no
PATH, no `setup`. `setup --print` had been promising this "once released".
One bundle serves every Claude Desktop platform. MCPB's `platform_overrides`
key on the OS alone, not the CPU, so a single bundle cannot pick between the
Apple Silicon and Intel builds; it carries a universal macOS binary instead,
the two release builds joined with `lipo`, beside the Windows binary. Each
slice is byte-identical to its attested archive, so the arm64 slice keeps
its ad-hoc signature, which Apple Silicon requires before it will execute
anything. Claude Desktop appends `.exe` itself for binary servers, so the
manifest names the binary once with no Windows override.
.github/scripts/build-mcpb.sh assembles it from the release's own published
archives, each checked against its .sha256 (with a `shasum` fallback, since
macOS has no `sha256sum`), and fills the manifest's version and tool list —
the tools read off the shipped binary's `tools/list`, using the server's own
display titles, so the install dialog cannot advertise a surface the server
lacks. The manifest's fixed fields live in crates/imcp2-local/mcpb/, with
the repository's ICP logo as the icon and the published privacy policy, which
Anthropic's directory requires of any listing.
It runs as a second dist `post-announce-jobs` entry, on macOS for native
`lipo` and to execute the binary it just built, and gives the bundle its own
provenance attestation: it is a new artifact with its own digest. That needs
`id-token`/`attestations` beyond the workflow's `contents: write`, granted
through `github-custom-job-permissions`; dist emits them on the caller job.
The release workflow was regenerated with dist 0.31.0.
The bundle is not code-signed yet. The README says so, and what that means:
an unverified-developer warning, possibly a Gatekeeper refusal on the
server's first macOS launch, and an outright block in organizations that
limit Claude Desktop to directory-listed extensions. It also documents
verifying the bundle, whose attestation names the bundle workflow as its
signer rather than the release workflow. The design doc now states the
single-bundle design in place of one artifact per platform.
Built locally against the published 0.5.0 archives: manifest schema passes
`mcpb validate`, 12 tools, both macOS slices, 0755 on the macOS binary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
aterga
changed the title
Explain the install scripts in the release notes, and document a verified install
Ship a Claude Desktop bundle with every local-MCP release, and explain the install scripts
Sep 24, 2026
…PE bugs
The bundle job checked each input archive only against a .sha256 fetched
from the same mutable release, which catches corruption but not
replacement — and then attested the bundle, so a swapped archive would have
come out wearing a fresh, legitimate attestation. Every input now has to
pass `gh attestation verify` against the release workflow with
`--source-ref refs/tags/<tag>`, which also refuses a genuinely attested
archive from an older release, before it is used. Tested against the
published 0.5.0 archives: a genuine archive passes, one with a byte appended
fails (no attestation for its digest), and a genuine one under the wrong tag
fails on the ref. The check is mandatory in CI; a local build may opt out
with MCPB_ALLOW_UNATTESTED=1, which is refused when CI is set.
The notes job tested for its marker with `printf | grep -q` under pipefail.
Once grep matches and exits, printf takes SIGPIPE on a body larger than the
pipe buffer, the test reads as "absent", and a re-run prepends the note
again — reproduced deterministically, 20 of 20 runs, with a 2 MB body. It is
now a shell substring match. The same bug class was also in the bundle
script's `find | head` and the README's `gh api | grep -m1` lookup, both
now `awk`, which reads its whole input; the README command resolves the tag
correctly even under `set -eo pipefail`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
…onour the opt-out
The README's verified install checked that the archive came from the
release workflow, but not from the release it had just resolved. Archive
names repeat across releases, so an older release's genuinely attested
archive swapped into the newest release would have passed: the downgrade
the bundle job already refuses for its own inputs. The verified install,
the bundle's check and the "Verifying a download" command now all carry
`--source-ref refs/tags/<tag> --deny-self-hosted-runners`. The release note
no longer calls dist's repository-only command what establishes
provenance; it points at the README's tag-bound check instead, and the
design doc describes the pinned form.
Verified against the published 0.5.0 archives: all five pass the tag-bound
check, and a wrong tag fails on the ref. The README's verified-install
block, extracted verbatim and run under `set -euo pipefail`, resolves the
tag, verifies, installs, and the installed binary reports 0.5.0.
MCPB_ALLOW_UNATTESTED=1 was reachable only when `gh` was missing, so on a
machine with `gh` installed the documented opt-out did nothing. It is now
checked first, and in CI it fails the build outright instead of being
ignored. All four cases were run: the opt-out with a `gh` that fails every
call builds the bundle with zero calls, the opt-out under CI=1 and a
missing `gh` both refuse, and a normal build verifies all four inputs
against the tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
The release note and the README said the shell installer skips its
checksum "silently" where there is no sha256sum. It doesn't: the 0.5.0
installer prints "skipping sha256 checksum verification (it requires the
'sha256sum' command)" (its `say` is on unless the install is run quiet)
and then installs anyway. Both now say that.
The same sentence pinned the gap on "stock macOS". That holds for older
releases; newer macOS releases have been reported to ship a sha256sum, and
the wording no longer depends on which. It names older macOS releases as
one case rather than the case. The bundle script's comment on its shasum
fallback is reworded to match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
This covers two points from Copilot's review summaries that never became inline threads.
The installer's checksum skip is not silent. This came from the 90d789f review, under "previously missed", for both the release note and the README. Copilot is right: when sha256sum is missing, the 0.5.0 installer prints skipping sha256 checksum verification (it requires the 'sha256sum' command) and installs anyway. Its say is on unless the install runs quiet. Fixed in e06c734 (pushing right after this comment): both texts now say it prints a one-line notice and installs anyway.
The same sentence blamed "stock macOS". That holds for older releases, but newer ones have been reported to ship a sha256sum, so the wording now names older macOS releases as one case rather than the only one.
secrets: inherit on the generated caller jobs. This came from the 026028a review summary. I'm leaving it as dist generates it, for three reasons:
dist 0.31.0 hard-codes secrets: inherit for custom jobs. It has github-custom-job-permissions but no secrets counterpart.
Dropping it by hand would need allow-dirty = ["ci"], which turns off dist's check that the release workflow still matches its config. That check is what keeps the pipeline reproducible from dist-workspace.toml.
It grants nothing an inline job wouldn't have. Both callees are local files at the same commit as the caller, and any job in a workflow can already name any repository secret. Neither callee names anything beyond secrets.GITHUB_TOKEN.
… the MCPB CLI
The bundle job held a write-scoped token and, through id-token: write, the
OIDC request credentials, and in that same environment it ran the
release's binary and `npx`, meaning the mcpb CLI and every npm dependency
npx resolved. A compromised dependency could have edited the release, or
minted a token and signed attestations as this workflow.
Unsetting the variables before those programs run narrows that but does not
close it: a child can still read its parent's original environment
(/proc/<ppid>/environ on Linux, `ps -E` on macOS). So the workflow is now
two jobs split by privilege. `build` gets `contents: read` and
`attestations: read`, no OIDC, and runs the third-party code; `publish`
gets the write and signing rights and runs only pinned actions and `gh`,
receiving the bundle as an artifact. The script also drops credentials from
its environment after its last fetch, which keeps a developer's own token
out of a local build's subprocesses.
`npx -y @anthropic-ai/mcpb@2.1.2` pinned only the top-level package; the
rest of the tree was resolved afresh on every release. The CLI now comes
from .github/scripts/mcpb-cli/package-lock.json (55 packages, every one
with an integrity hash and resolved from registry.npmjs.org, none with an
install script), installed with `npm ci --ignore-scripts` into the scratch
directory and invoked from there. MCPB_VERSION goes: the lockfile is the
version.
Tested with a full verified build against the 0.5.0 release, with shims
around python3, npm and node recording which credentials reached them:
none did, all four attestation checks ran before the scrub, and the
bundle's extracted contents are identical to the npx-built one's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
The notes job and the bundle's publish job ran on ubuntu-22.04, copied from
the dist-generated workflow's default; this repository's own workflows use
ubuntu-24.04, and so do these now. The bundle's build job moves from
macos-14 to macos-15. Nothing in either job depends on the image beyond
bash, jq, gh, python3, node/npm and lipo, which all of them carry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
Verify universal-binary slices match their attested source binaries
.github/scripts/build-mcpb.sh:131
The PR states that each universal-binary slice is checked byte-for-byte with cmp, but this code only verifies that the arm64 and x86_64 architecture labels exist. That leaves the signature-preservation invariant unenforced: a lipo result whose slice bytes differ from the attested inputs would still be packaged and published. Extract both slices and compare them to their source binaries before continuing.
The bundle's safety argument rests on lipo carrying each slice through
unchanged: that keeps the arm64 slice's ad-hoc signature valid, and it is
what the archives' attestations vouch for. The script only checked that
both architecture names were present, so a lipo that rewrote a slice would
still have been packaged and attested. It now extracts each slice with
`lipo -thin` and compares it byte for byte with the binary from the
attested archive, stopping the build on any difference (a missing slice
already fails `-thin`).
Tested: a full verified build against 0.5.0 passes, and a lipo wrapper
that alters the thinned x86_64 slice stops the build with no bundle
written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
This covers two points from Copilot's latest review summaries, which have no inline threads.
Universal-binary slices weren't checked against their inputs (1e7ac1d summary, "previously missed"). Correct: the script only confirmed that both architecture names were present, while the description says each slice is byte-identical to its attested binary. I had checked that by hand, but the build didn't enforce it. Fixed in e8aa97e (pushing right after this comment): after lipo -create, each slice is extracted with lipo -thin and compared with cmp against the binary from its attested archive, and any difference stops the build. A missing slice already fails -thin.
Tested against 0.5.0: a full verified build passes, and a lipo wrapper that alters the thinned x86_64 slice stops the build with no bundle written.
Job ordering and duplicate notes (29ff585 summary). There is no code change here, and there are two parts to it:
Ordering: dist wires every post-announce job to [plan, announce] and has no way to order custom jobs against each other, short of hand-editing the generated workflow, which dist's own check rejects. So the notes job and the bundle workflow run in parallel. For the few minutes the bundle takes to build, the note mentions an .mcpb that isn't attached yet. If the bundle job fails, the release run shows that, and re-running it attaches the bundle.
Duplicates: the notes job is idempotent through its marker, and the upload uses --clobber, so re-runs don't duplicate anything. Two runs can't overlap for the same tag in practice. A run can't be re-run while it's still in progress, and a second run would need the tag pushed again, which dist's host job refuses once the release exists.
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
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.
Summary
Two pieces of the local-MCP install story, both riding dist's custom-job seam in the release pipeline:
imcp2-localrelease now shipsimcp2-local.mcpb— download, double-click, and Claude Desktop installs and manages the server. No PATH, nosetup.setup --printhad been promising this "once released".The bundle
One bundle covers every Claude Desktop platform. MCPB's
platform_overrideskey on the OS alone, not the CPU, so a single bundle cannot choose between the Apple Silicon and Intel builds. It carries a universal macOS binary instead — the two release builds joined withlipo— beside the Windows binary. dist has no universal-binary support, so this is the one genuinely new build step.Each slice is byte-identical to its attested archive's binary: the build script extracts each slice with
lipo -thin, compares it withcmp, and stops on any difference. This is what keeps the arm64 slice's ad-hoc signature valid, and Apple Silicon won't execute arm64 code without one.Every input archive must carry the release workflow's attestation for that very tag before it is used. The job goes on to attest the bundle, so a swapped input would otherwise come out wearing a fresh, legitimate attestation.
Building and publishing are separate jobs, split by privilege. Building runs code this repository didn't write (the MCPB CLI and its npm dependencies), so that job holds only a read-only token and no OIDC. A second job, which runs nothing but pinned actions and
gh, attests the bundle and attaches it to the release.Claude Desktop appends
.exeitself for binary servers, so the manifest names the binary once, with no Windows override.The manifest's tool list is read off the shipped binary's own
tools/list, using the server's display titles, so the install dialog can't advertise a surface the server lacks:Fixed manifest fields live in
crates/imcp2-local/mcpb/, with the repository's ICP logo (512×512, the size Claude Desktop recommends) and the published privacy policy, which Anthropic's directory requires of any listing. Display name and keywords follow the test bundle you built by hand earlier; adjust freely.It is not code-signed yet, and the README says what that means: an unverified-developer warning; possibly a Gatekeeper refusal on the server's first macOS launch, since a browser download is quarantined and the binary isn't notarized; and an outright block in organizations that limit Claude Desktop to directory-listed extensions, which is what your own install attempt hit. Signing stays blocked on the organization's Developer ID credentials.
The release-note summary
The summary is prepended to every future release's notes, from
.github/release-notes/imcp2-local-install-note.md. What it says is what I found by reading the shipped installers:~/.cargo/binwith an auto-updater and put that directory on PATH. The shell script appends to every shell profile it finds; the PowerShell one edits thePathregistry key.sha256sumis missing (older macOS releases, for one), the shell script prints a one-line notice and installs without checking its checksum. The PowerShell installer checks none.It then points at
imcp2-local setup/--print/--remove, and at the bundle for Claude Desktop users.Related issues
Follows the 0.5.0 release, the first
imcp2-localbinary release.Changes
.github/scripts/build-mcpb.shassembles the bundle from the release's own published archives..sha256, falling back toshasumwheresha256sumis missing so it doesn't repeat the installer's gap.gh attestation verify --signer-workflow …/imcp2-local-release.yml --source-ref refs/tags/<tag> --deny-self-hosted-runners. That check is mandatory in CI;MCPB_ALLOW_UNATTESTED=1opts out locally only and fails the build underCI.lipo -create, each slice iscmp-checked against its attested input.GH_TOKEN,GITHUB_TOKENand the OIDC request variables from its environment.mcpb validate+pack, using the CLI installed from the lockfile below..github/scripts/mcpb-cli/package.json+package-lock.jsonpin the MCPB CLI's whole dependency tree: 55 packages, every one with an integrity hash and resolved fromregistry.npmjs.org, none with an install script. The script installs them withnpm ci --ignore-scriptsinto its scratch directory and invokes the local CLI, with nonpx..github/workflows/imcp2-local-mcpb.ymldefines two jobs:build(macos-15,contents: read+attestations: read, no OIDC) runs the script. macOS provides the nativelipo, and the job executes the binary it just built to read the tool list.publish(ubuntu-24.04, write +id-token+attestations) receives the bundle as an artifact, attests it as the new artifact it is, and attaches it to the release..github/workflows/imcp2-local-install-note.yml+.github/release-notes/imcp2-local-install-note.md— the notes job (ubuntu-24.04), made idempotent by a marker line that is matched in the shell rather than through a pipe, so the check stays reliable underpipefail.dist-workspace.toml— both jobs underpost-announce-jobs, plusgithub-custom-job-permissionsgranting the bundle workflowid-token/attestationsbeyond the defaultcontents: write. dist puts that union on the caller job, and each of the bundle's two jobs takes only its share..github/workflows/imcp2-local-release.yml— regenerated with dist 0.31.0, never hand-edited.crates/imcp2-local/README.md— the Install section, in order:imcp2-local-v*tag by paginating the releases API. (releases/latestis unusable here: production deploys publishrelease-*releases in this repo.) The steps are&&-chained withcurl -f, so a failed attestation stops the install.Every verification command the README documents, for the archives and the bundle alike, is pinned to the release's tag with
--source-refand to GitHub-hosted runners. Archive names repeat across releases, so an older release's attested file would otherwise pass for a newer one's.crates/imcp2-local/src/setup.rs—setup --printpoints at the shipped bundle.docs/scoping-local-deployment.md— describes the single-bundle design, replacing one artifact per platform, together with its privilege-split assembly and the tag-pinned verification.Testing
llvm-lipo:mcpb validatepasses the schema.icon.png,manifest.json,server/imcp2-local(mode 0755,x86_64 arm64) andserver/imcp2-local.exe(PE32+ x86-64).LC_CODE_SIGNATUREand is byte-identical to the attested release binary.lipo. Alipowrapper that alters the thinned x86_64 slice stops the build with no bundle written.ghagainst the published 0.5.0 archives:ghcalls.CI.ghrefuses.python3(which launches the binary),npmandnode:GH_TOKEN,GITHUB_TOKENor the two OIDC variables.npx-built one's.set -euo pipefailin a sandboxedHOME, resolvesimcp2-local-v0.5.0, verifies it and installs it. The installed binary reports0.5.0.dist generate --checkclean; all three workflow files parse as YAML; the generated caller job carries the three permissions.gh:printf | grep -qmissed it 20 of 20 times underpipefail.imcp2-local-v0.5.0pastv0.5.0andrelease-2026-08-18-0.5.0.cargo fmt --all --check,cargo test --workspace --all-targets(297 passed); clippy's remainingmap_orwarnings are pre-existing inimcp2-core..github/scripts/scan-internal-identifiers.sh— clean.imcp2-local-v*tag. The bundle workflow is the part most worth watching on that release.Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK