Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/release-notes/imcp2-local-install-note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- install-script-summary -->
> **What the install scripts below do:** each downloads the `imcp2-local` binary for your platform from this release, installs it plus an auto-updater into `~/.cargo/bin`, and adds that directory to your PATH — the shell script by appending a line to every shell profile it can find, the PowerShell script by editing your `Path` registry key. The shell script also compares a checksum baked into itself, but skips that check silently on stock macOS, which has no `sha256sum`; the PowerShell script does not check one at all. Where that shell checksum does run it ships inside the very script being piped to a shell, so on either platform the attestation commands at the end of these notes are what establish provenance — `IMCP2_LOCAL_NO_MODIFY_PATH=1` and `IMCP2_LOCAL_DISABLE_UPDATE=1` opt out of the PATH edits and the updater.
>
> **Then connect it to your AI tool:** `imcp2-local setup` registers the server with the clients on this machine — Claude Desktop, Claude Code, Codex, Cursor, Antigravity — and prints Perplexity's UI steps; `imcp2-local setup --print` shows each client's steps without writing anything, and `imcp2-local setup --remove` undoes them. Restart the client afterwards. On Claude Desktop you can skip both steps: double-click `imcp2-local.mcpb` from this release (it is not yet code-signed, so expect an unverified-developer warning). The [README](https://github.com/dfinity/imcp2/blob/main/crates/imcp2-local/README.md#register-it-with-your-ai-tools) lists the per-client registration each one receives.
152 changes: 152 additions & 0 deletions .github/scripts/build-mcpb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Build the Claude Desktop bundle (.mcpb) for one imcp2-local release.
#
# .github/scripts/build-mcpb.sh <tag> <out-dir>
# e.g. .github/scripts/build-mcpb.sh imcp2-local-v0.5.0 dist-mcpb
#
# The bundle is assembled from the release's own published archives, so it
# carries exactly the binaries that release attests, each checked against the
# release's .sha256 before use:
#
# server/imcp2-local universal macOS binary (arm64 + x86_64)
# server/imcp2-local.exe Windows x64
#
# Why universal: MCPB's `platform_overrides` key on the OS alone
# (darwin/win32/linux), not the CPU, so one bundle cannot choose between the
# Apple Silicon and Intel builds. One fat Mach-O runs on both. Windows needs
# no override: for binary servers Claude Desktop appends `.exe` to the
# command itself, so the manifest names the file without it.
#
# The manifest's version and tool list are filled in here, the tools from the
# release's own binary answering `tools/list`, so the install dialog can
# never advertise a surface the shipped server doesn't have.
#
# Env:
# LIPO lipo implementation (default `lipo`; `llvm-lipo` works off macOS)
# MCPB_VERSION @anthropic-ai/mcpb CLI version (pinned below)
set -euo pipefail

tag="${1:?usage: build-mcpb.sh <tag> <out-dir>}"
out="${2:?usage: build-mcpb.sh <tag> <out-dir>}"
case "$tag" in
imcp2-local-v*) ;;
*) echo "not an imcp2-local release tag: $tag" >&2; exit 2 ;;
esac
version="${tag#imcp2-local-v}"
base="https://github.com/dfinity/imcp2/releases/download/$tag"
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
lipo="${LIPO:-lipo}"
mcpb="@anthropic-ai/mcpb@${MCPB_VERSION:-2.1.2}"

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

# macOS ships `shasum`, not `sha256sum` — the very gap the release notes warn
# about in the installer, so this does not repeat it.
sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}

# Download one release asset and refuse it unless it matches the release's
# published checksum.
fetch() {
local asset="$1" want got
curl -fsSL --retry 3 -o "$work/$asset" "$base/$asset"
curl -fsSL --retry 3 -o "$work/$asset.sha256" "$base/$asset.sha256"
want="$(awk '{print $1}' "$work/$asset.sha256")"
got="$(sha256 "$work/$asset")"
Comment thread
aterga marked this conversation as resolved.
if [ "$want" != "$got" ]; then
echo "checksum mismatch for $asset: want $want, got $got" >&2
exit 1
fi
}

fetch imcp2-local-aarch64-apple-darwin.tar.xz
fetch imcp2-local-x86_64-apple-darwin.tar.xz
fetch imcp2-local-x86_64-pc-windows-msvc.zip
for t in aarch64-apple-darwin x86_64-apple-darwin; do
tar -xJf "$work/imcp2-local-$t.tar.xz" -C "$work"
done
mkdir -p "$work/win"
unzip -q "$work/imcp2-local-x86_64-pc-windows-msvc.zip" -d "$work/win"

bundle="$work/bundle"
mkdir -p "$bundle/server"
"$lipo" -create -output "$bundle/server/imcp2-local" \
"$work/imcp2-local-aarch64-apple-darwin/imcp2-local" \
"$work/imcp2-local-x86_64-apple-darwin/imcp2-local"
chmod 0755 "$bundle/server/imcp2-local"
archs="$("$lipo" -archs "$bundle/server/imcp2-local")"
for a in arm64 x86_64; do
case " $archs " in
*" $a "*) ;;
*) echo "universal binary is missing the $a slice (has: $archs)" >&2; exit 1 ;;
esac
done

exe="$(find "$work/win" -type f -name 'imcp2-local.exe' | head -n 1)"
if [ -z "$exe" ]; then
echo "no imcp2-local.exe in the Windows archive" >&2
exit 1
fi
cp "$exe" "$bundle/server/imcp2-local.exe"
cp "$repo_root/crates/imcp2-local/mcpb/icon.png" "$bundle/icon.png"

# A binary this host can execute, to ask the shipped server for its tools.
case "$(uname -s)-$(uname -m)" in
Darwin-*) host_bin="$bundle/server/imcp2-local" ;;
Linux-x86_64 | Linux-aarch64)
t="$(uname -m)-unknown-linux-gnu"
fetch "imcp2-local-$t.tar.xz"
tar -xJf "$work/imcp2-local-$t.tar.xz" -C "$work"
host_bin="$work/imcp2-local-$t/imcp2-local"
;;
*) echo "cannot introspect the tool list on $(uname -s)-$(uname -m)" >&2; exit 1 ;;
esac

tools="$(python3 - "$host_bin" <<'PY'
Comment thread
aterga marked this conversation as resolved.
import json, os, re, subprocess, sys

proc = subprocess.Popen(
[sys.argv[1]], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1,
env=dict(os.environ, IMCP2_NO_OPEN="1", RUST_LOG="error"),
)

def call(rid, method, params):
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": rid, "method": method, "params": params}) + "\n")
proc.stdin.flush()
return json.loads(proc.stdout.readline())

call(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "build-mcpb", "version": "0"}})
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
proc.stdin.flush()
listed = call(2, "tools/list", {})["result"]["tools"]
proc.kill()

def summary(tool):
# The server's own display title where it has one ("Get Candid
# interface"); otherwise the description's first sentence.
title = (tool.get("annotations") or {}).get("title") or tool.get("title")
if title:
return title
text = " ".join((tool.get("description") or "").split())
first = re.match(r"(.+?[.!?])(?:\s|$)", text)
return (first.group(1) if first else text)[:240]

print(json.dumps([{"name": t["name"], "description": summary(t)} for t in listed]))
PY
)"

jq --arg version "$version" --argjson tools "$tools" \
'.version = $version | .tools = $tools' \
"$repo_root/crates/imcp2-local/mcpb/manifest.base.json" > "$bundle/manifest.json"

npx -y "$mcpb" validate "$bundle/manifest.json"
mkdir -p "$out"
npx -y "$mcpb" pack "$bundle" "$out/imcp2-local.mcpb"
Comment thread
aterga marked this conversation as resolved.
Outdated
echo "built $out/imcp2-local.mcpb for $tag ($(echo "$tools" | jq length) tools; macOS slices: $archs)"
61 changes: 61 additions & 0 deletions .github/workflows/imcp2-local-install-note.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Prepends a plain-language summary of what the install scripts do to the
# release notes `dist` writes for an `imcp2-local-v*` release.
#
# Why a job rather than a setting: dist builds the release body itself (the
# install one-liners, the download table, the attestation section) and has no
# config for extra prose — a custom job is the only documented seam. This one
# hangs off `post-announce-jobs` in dist-workspace.toml, so `dist generate`
# keeps wiring it in and the release workflow stays reproducible from config.
#
# The prose lives in .github/release-notes/imcp2-local-install-note.md so it
# is reviewed as rendered markdown rather than as a string inside YAML. Keep
# it in step with the installers and with the crate README's Install section.
#
# Runs after the release is published and edits the notes in place; the
# marker in that file makes a re-run a no-op.
name: imcp2-local install note

on:
workflow_call:
inputs:
plan:
required: true
type: string

jobs:
install-note:
runs-on: ubuntu-22.04
Comment thread
aterga marked this conversation as resolved.
Outdated
permissions:
contents: write
env:
PLAN: ${{ inputs.plan }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NOTE_FILE: .github/release-notes/imcp2-local-install-note.md
steps:
# Pinned to a commit SHA, as every other action in this pipeline is.
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

- name: Prepend the install-script summary to the release notes
shell: bash
run: |
set -euo pipefail

tag="$(printf '%s' "$PLAN" | jq -er '.announcement_tag')"
marker="$(head -n 1 "$NOTE_FILE")"

body="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json body -q .body)"
if printf '%s' "$body" | grep -qF "$marker"; then
Comment thread
aterga marked this conversation as resolved.
Outdated
echo "summary already present on $tag — nothing to do"
exit 0
fi

{
cat "$NOTE_FILE"
printf '\n%s\n' "$body"
} > "$RUNNER_TEMP/notes.md"

gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --notes-file "$RUNNER_TEMP/notes.md"
echo "install-script summary added to $tag"
62 changes: 62 additions & 0 deletions .github/workflows/imcp2-local-mcpb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Builds the Claude Desktop bundle (`imcp2-local.mcpb`) for an
# `imcp2-local-v*` release, attests it, and attaches it to that release.
#
# Hung off `post-announce-jobs` in dist-workspace.toml: dist has no notion of
# an MCPB artifact, and the bundle needs the finished per-platform archives
# anyway, so it is assembled after the release exists — from that release's
# own published archives, each checked against its .sha256 (see
# .github/scripts/build-mcpb.sh for why the macOS binary is universal).
#
# The bundle is a new artifact with its own digest, so it gets its own
# provenance attestation rather than leaning on the archives'; the job needs
# `id-token`/`attestations` for that, granted to its caller through
# `github-custom-job-permissions`.
#
# Runs on macOS for the native `lipo` and to execute the universal binary it
# just built, which is how the manifest's tool list is read off the server.
name: imcp2-local Claude Desktop bundle

on:
workflow_call:
inputs:
plan:
required: true
type: string

jobs:
mcpb:
runs-on: macos-14
permissions:
contents: write
id-token: write
attestations: write
env:
PLAN: ${{ inputs.plan }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
# Pinned to a commit SHA, as every other action in this pipeline is.
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

- name: Build the bundle from this release's archives
shell: bash
run: |
set -euo pipefail
tag="$(printf '%s' "$PLAN" | jq -er '.announcement_tag')"
echo "TAG=$tag" >> "$GITHUB_ENV"
.github/scripts/build-mcpb.sh "$tag" mcpb-out

- name: Attest
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f
with:
subject-path: mcpb-out/imcp2-local.mcpb

- name: Attach to the release
shell: bash
run: |
set -euo pipefail
# --clobber so a re-run replaces the asset rather than failing on it.
gh release upload "$TAG" mcpb-out/imcp2-local.mcpb \
--repo "$GITHUB_REPOSITORY" --clobber
22 changes: 22 additions & 0 deletions .github/workflows/imcp2-local-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,25 @@ jobs:
with:
persist-credentials: false
submodules: recursive

custom-imcp2-local-install-note:
needs:
- plan
- announce
uses: ./.github/workflows/imcp2-local-install-note.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

custom-imcp2-local-mcpb:
needs:
- plan
- announce
uses: ./.github/workflows/imcp2-local-mcpb.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
permissions:
"attestations": "write"
"contents": "write"
"id-token": "write"
72 changes: 68 additions & 4 deletions crates/imcp2-local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,71 @@ cannot spawn local processes; they keep using the hosted server.
## Install

Release binaries (macOS arm64/x64, Linux x64/arm64, Windows x64) ship from
this repository's GitHub releases with shell/PowerShell installers, built by
`dist` from `imcp2-local-v*` tags. Until the first release is cut, build from
source:
this repository's GitHub releases, built by `dist` from `imcp2-local-v*` tags.

**Verified install.** This binary acts as your Internet Identity, so prefer the
path that establishes where the artifact came from. Download the archive, check
its provenance against the workflow that built it, then install it into a
directory on your `PATH`:

```sh
# Resolve the newest binary release. `releases/latest` is NOT this crate's:
# production deploys publish `release-*` releases in this same repository, so
# the repository's latest release is usually one of those. Paginate rather
# than take a first page, for the same reason — this crate's tag is a small
# minority of the releases here.
TAG=$(gh api --paginate repos/dfinity/imcp2/releases --jq '.[].tag_name' \
| grep -m1 '^imcp2-local-v')
TARGET=aarch64-apple-darwin # or x86_64-apple-darwin, {x86_64,aarch64}-unknown-linux-gnu

# Chained: a failed download or a failed attestation stops the install.
curl -fLO "https://github.com/dfinity/imcp2/releases/download/$TAG/imcp2-local-$TARGET.tar.xz" &&
gh attestation verify "imcp2-local-$TARGET.tar.xz" -R dfinity/imcp2 \
--signer-workflow dfinity/imcp2/.github/workflows/imcp2-local-release.yml &&
Comment thread
Copilot marked this conversation as resolved.
Outdated
tar xf "imcp2-local-$TARGET.tar.xz" &&
mkdir -p ~/.local/bin &&
install "imcp2-local-$TARGET/imcp2-local" ~/.local/bin/
```

`~/.local/bin` stands in for any directory already on your `PATH`; the last
two commands create it and copy the binary there, nothing edits your shell
configuration.

(Windows ships `imcp2-local-x86_64-pc-windows-msvc.zip`; verify it the same way.)

**Installer script.** Shorter, and what the release notes lead with. It
downloads the binary for your platform, installs it plus an auto-updater into
`~/.cargo/bin`, and adds that directory to your PATH by appending a line to
every shell profile it can find — `IMCP2_LOCAL_NO_MODIFY_PATH=1` and
`IMCP2_LOCAL_DISABLE_UPDATE=1` opt out of those two. The shell script also
compares a checksum baked into itself, but skips that silently on stock macOS,
which has no `sha256sum`; the PowerShell installer checks none at all. Even
where the shell checksum runs, it ships inside the very script being piped to
a shell, so it catches a corrupted download rather than a bad release. On both
platforms the attestation above is what establishes provenance.

```sh
# Substitute the newest imcp2-local-v* tag; each release's notes carry the
# current command, and `releases/latest` is not this crate's release (above).
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dfinity/imcp2/releases/download/imcp2-local-v0.5.0/imcp2-local-installer.sh | sh
```

**Claude Desktop bundle.** Each release also carries `imcp2-local.mcpb`.
Download it and double-click it: Claude Desktop installs and manages the
server itself — nothing lands on your `PATH`, and no `setup` is needed. It
holds a universal macOS binary (Apple Silicon and Intel) and the Windows one.
It is not yet code-signed, so expect Claude Desktop's unverified-developer
warning; on macOS, Gatekeeper may also refuse the server's first launch until
you allow it under System Settings → Privacy & Security. Organizations that
limit Claude Desktop to directory-listed extensions block it outright. The
bundle is attested like the archives, but by its own workflow:

```sh
gh attestation verify imcp2-local.mcpb -R dfinity/imcp2 \
--signer-workflow dfinity/imcp2/.github/workflows/imcp2-local-mcpb.yml
Comment thread
aterga marked this conversation as resolved.
Outdated
```

**From source.**

```sh
cargo build --release -p imcp2-local
Expand Down Expand Up @@ -106,7 +168,9 @@ screen. Concretely:
## Verifying a download

Every platform archive carries a keyless provenance attestation proving it
was built by this repository's release workflow:
was built by this repository's release workflow (the Claude Desktop bundle is
attested the same way by `imcp2-local-mcpb.yml`, which assembles it — see
Install):

```sh
# (Windows archives are .zip — substitute the extension.)
Expand Down
Binary file added crates/imcp2-local/mcpb/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading