Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ jobs:
# `--locked` fails if Cargo.lock is out of date, so CI also guards the
# dependency pins (e.g. ic-agent / ic-transport-types) from silent drift.
- name: Build
run: cargo build --locked --all-targets
run: cargo build --locked --workspace --all-targets

- name: Test
run: cargo test --locked --all-targets
run: cargo test --locked --workspace --all-targets

# Block new internal network identifiers from entering the repository. Secret
# scanning and push protection already cover credentials; this covers the
Expand Down
73 changes: 47 additions & 26 deletions .github/workflows/publish-crate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
# Environment name release <- must match the `environment:` below,
# or be left blank to accept any
# A trusted publisher can only be configured on a crate that already exists, so the
# FIRST release (0.1.0) has to be published by hand — `cargo publish --locked` with
# a personal token — and every release after it goes through this workflow.
# FIRST release of each crate has to be published by hand — `cargo publish --locked`
# with a personal token (imcp2's 0.1.0 was; imcp2-core's first release must be, and
# its trusted publisher configured with this same repository/workflow/environment)
# — and every release after it goes through this workflow.
#
# REQUIRED REPOSITORY SETTINGS, to be in place BEFORE the trusted publisher is
# enabled. Neither can be expressed in this file, and without them the guards below
Expand Down Expand Up @@ -101,20 +103,24 @@ jobs:
rustup default stable
rustc --version && cargo --version

# A tag that disagrees with the manifest publishes a version under a name
# nobody can check out — and crates.io cannot take it back. Read the version
# from cargo itself rather than grepping the manifest, so the check reads the
# same field `cargo publish` will.
- name: Check the tag matches Cargo.toml
# A tag that disagrees with the manifests publishes a version under a name
# nobody can check out — and crates.io cannot take it back. Read the versions
# from cargo itself rather than grepping the manifests, so the check reads the
# same field `cargo publish` will. Both workspace crates release in lockstep:
# one tag, one version, two packages (imcp2 depends on imcp2-core "=version"
# semantics via the workspace pin, so a skew would not resolve anyway).
- name: Check the tag matches both Cargo.toml versions
run: |
tag="${GITHUB_REF_NAME#v}"
manifest="$(cargo metadata --format-version 1 --no-deps |
jq -er '.packages[] | select(.name == "imcp2") | .version')"
echo "tag=$tag manifest=$manifest"
if [ "$tag" != "$manifest" ]; then
echo "::error::tag ${GITHUB_REF_NAME} does not match Cargo.toml version ${manifest}" >&2
exit 1
fi
for pkg in imcp2-core imcp2; do
manifest="$(cargo metadata --format-version 1 --no-deps |
jq -er --arg pkg "$pkg" '.packages[] | select(.name == $pkg) | .version')"
echo "tag=$tag $pkg=$manifest"
if [ "$tag" != "$manifest" ]; then
echo "::error::tag ${GITHUB_REF_NAME} does not match ${pkg}'s Cargo.toml version ${manifest}" >&2
exit 1
fi
done

# A tag can name any commit in the repository, including one that never went
# through review. This catches the mistake — tagging a topic branch, or a
Expand All @@ -135,15 +141,21 @@ jobs:
# immutable public release. `--locked` also guards Cargo.lock from drift, and
# the lockfile is what ships in the .crate.
- name: Test
run: cargo test --locked --all-targets
run: cargo test --locked --workspace --all-targets

# Rehearses the real thing — packages the crate and compiles the package in
# isolation — but stops short of uploading, so a package that only builds
# because of something untracked in the working tree fails here rather than
# after the upload is already permanent. This is also the verify build that
# lets the publish job skip its own; see `--no-verify` below.
# Rehearses the real thing — packages each crate, and compiles imcp2-core's
# package in isolation — but stops short of uploading, so a package that only
# builds because of something untracked in the working tree fails here rather
# than after the upload is already permanent. imcp2's package cannot be
# compile-verified before this release's imcp2-core exists on crates.io (it
# depends on it by version), so its rehearsal is `cargo package` without the
# verify build: the workspace test above already compiled exactly these
# sources. This is also the verify work that lets the publish job skip its
# own; see `--no-verify` below.
- name: Package
run: cargo publish --locked --dry-run
run: |
cargo publish --locked --dry-run -p imcp2-core
cargo package --locked --no-verify -p imcp2

# Holds the credential, and compiles nothing.
publish:
Expand Down Expand Up @@ -177,10 +189,19 @@ jobs:
# it `cargo publish` compiles the package — running the dependency tree's build
# scripts and proc macros — while CARGO_REGISTRY_TOKEN sits in their
# environment. The verify build is not skipped, only moved: the `verify` job
# ran `cargo publish --locked --dry-run` over this same commit, which packages
# and compiles exactly what is uploaded here. What remains in this job is
# reading files into a .crate and an HTTPS upload.
- name: Publish
run: cargo publish --locked --no-verify
# packaged both crates (and compiled imcp2-core's package) over this same
# commit, and the workspace test suite compiled exactly these sources. What
# remains in this job is reading files into a .crate and an HTTPS upload.
#
# Order matters: imcp2 depends on imcp2-core by version, so core publishes
# first; `cargo publish` waits for the just-published crate to appear in the
# index before returning, so the second publish resolves the first.
- name: Publish imcp2-core
run: cargo publish --locked --no-verify -p imcp2-core
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

- name: Publish imcp2
run: cargo publish --locked --no-verify -p imcp2
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
54 changes: 25 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 49 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,27 @@ categories = [
# Everything the library and the bundled binary compile from ships; the
# deployment scaffolding (container, systemd/native deploy, dashboards, CI,
# submission docs) is repository-only and would just inflate the .crate.
# `src/assets` and `static` are NOT optional: both are pulled in at compile
# time with `include_str!`.
# `src/assets` is NOT optional: it is pulled in at compile time with
# `include_str!`. `crates/` holds the other workspace members (each packaged
# on its own), so it is excluded from THIS package.
exclude = [
".github/",
".dockerignore",
"Dockerfile",
"crates/",
"deploy/",
"docs/",
"monitoring/",
]

[dependencies]
rmcp = { version = "1.7", features = [
"server",
"macros",
"transport-streamable-http-server",
"auth",
] }
[workspace]
members = [".", "crates/imcp2-core"]

# Single source of truth for the version pins both crates share, so the agent,
# codec, and candid lines can never drift between imcp2-core and imcp2.
[workspace.dependencies]
imcp2-core = { path = "crates/imcp2-core", version = "0.2.0" }
rmcp = "1.7"
# The Agent is INJECTED by the embedding application (see McpServer::new), so a
# host shares one boundary-node client and one ic-agent across the process.
# Deliberately a direct dependency for now: sourcing ic-agent through ic-bn-lib
Expand All @@ -51,12 +54,40 @@ ic-agent = "0.49"
# worker-blocking CPU (ICPBB-382); 0.10.31 made it O(n).
candid = { version = "0.10.31", features = ["value"] }
candid_parser = "0.3.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time"] }
axum = "0.8"
anyhow = "1"
tokio = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
sha2 = "0.11.0"
base64 = "0.22.1"
hex = "0.4.3"
crc32fast = "1"
regex = "1.12.4"
reqwest = { version = "0.13", default-features = false, features = ["rustls"] }
getrandom = "0.3.4"
urlencoding = "2.1.3"
url = "2"
http = "1"

[dependencies]
# The transport/OAuth-agnostic components (the tool surface, the II session
# engine, the connect-handshake primitives) live in imcp2-core; this crate adds
# the hosted composition: the OAuth 2.1 authorization server, the
# streamable-HTTP transport, and the axum routers.
imcp2-core = { workspace = true }
rmcp = { workspace = true, features = [
"server",
"macros",
"transport-streamable-http-server",
] }
ic-agent = { workspace = true }
candid = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "time"] }
axum = "0.8"
anyhow = "1"
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio-util = "0.7"
uuid = { version = "1", features = ["v4"] }
Expand All @@ -67,15 +98,12 @@ tower-http = { version = "0.7", features = ["cors"] }
# estate. The `process` feature adds CPU / RSS / file-descriptor collectors on
# Linux, which is the deploy target.
prometheus = { version = "0.14", features = ["process"] }
sha2 = "0.11.0"
base64 = "0.22.1"
hex = "0.4.3"
crc32fast = "1"
regex = "1.12.4"
reqwest = { version = "0.13", default-features = false, features = ["rustls"] }
getrandom = "0.3.4"
urlencoding = "2.1.3"
url = "2"
sha2 = { workspace = true }
base64 = { workspace = true }
hex = { workspace = true }
getrandom = { workspace = true }
urlencoding = { workspace = true }
url = { workspace = true }
# Only for the PocketIC-backed end-to-end test (src/e2e_handshake.rs), behind
# the `e2e` feature so the default build never compiles it. Client library for a
# local Internet Identity canister; version tracks the `pocket-ic` server binary.
Expand Down
11 changes: 5 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Multi-stage build for the MCP server. The WASM Candid codec under static/wasm
# is prebuilt and committed, so no wasm toolchain is needed here.
# Multi-stage build for the MCP server. Everything the binary serves is
# compiled in via include_str!, so the runtime image carries only the binary.
FROM rust:1-slim-bookworm AS build
WORKDIR /app
# GIT_SHA / BUILD_TIME are baked into the binary (option_env! in main.rs) and
Expand All @@ -11,8 +11,9 @@ ENV GIT_SHA=${GIT_SHA}
ENV BUILD_TIME=${BUILD_TIME}
COPY Cargo.toml Cargo.lock ./
COPY src ./src
# static/ is needed at build time too: tools.rs include_str!s the candid/OQL references.
COPY static ./static
# The workspace member crates (imcp2-core carries the tool surface and its
# compiled-in candid/OQL references under crates/imcp2-core/static).
COPY crates ./crates
RUN cargo build --release

FROM debian:bookworm-slim
Expand All @@ -21,8 +22,6 @@ WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/release/imcp2 /usr/local/bin/imcp2
# Static assets (signing frontend + WASM codec) are served relative to the workdir.
COPY static ./static
# See deploy/native/imcp2.service: the per-request log line is debug-level, and
# is worth keeping on a deployed host.
ENV RUST_LOG=info,imcp2::metrics=debug
Expand Down
36 changes: 36 additions & 0 deletions crates/imcp2-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "imcp2-core"
version = "0.2.0"
edition = "2021"
description = "Transport-agnostic components of the IMCP2 Internet Computer MCP server: the tool surface, the Internet Identity session engine, and the II connect-handshake primitives."
license = "Apache-2.0"
repository = "https://github.com/dfinity/imcp2"
homepage = "https://mcp.internetcomputer.org"
keywords = ["mcp", "internet-computer", "icp", "candid"]
categories = ["network-programming", "api-bindings"]

[dependencies]
# No transport feature: each binary picks its own (streamable-HTTP in imcp2,
# stdio in imcp2-local). `server` + `macros` carry the tool router, the
# ServerHandler machinery, and the schemars re-export every tool schema
# derives against.
rmcp = { workspace = true, features = ["server", "macros"] }
ic-agent = { workspace = true }
candid = { workspace = true }
candid_parser = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "net"] }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
sha2 = { workspace = true }
base64 = { workspace = true }
hex = { workspace = true }
crc32fast = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true }
getrandom = { workspace = true }
urlencoding = { workspace = true }
url = { workspace = true }
# The request-extension type the Bearer session lookup reads (axum re-exports
# this same type; depending on `http` directly keeps axum out of this crate).
http = { workspace = true }
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading