diff --git a/.github/workflows/crates-publish.yml b/.github/workflows/crates-publish.yml index 7e46e7ee..6f3db75c 100644 --- a/.github/workflows/crates-publish.yml +++ b/.github/workflows/crates-publish.yml @@ -55,8 +55,16 @@ jobs: - name: Package and verify CLI crate working-directory: rust run: cargo publish --dry-run -p decided + - name: Package and verify MCP crate + working-directory: rust + run: cargo publish --dry-run -p decided-mcp - name: Publish CLI crate working-directory: rust env: CARGO_REGISTRY_TOKEN: ${{ steps.cargo-auth.outputs.token }} run: cargo publish -p decided + - name: Publish MCP crate + working-directory: rust + env: + CARGO_REGISTRY_TOKEN: ${{ steps.cargo-auth.outputs.token }} + run: cargo publish -p decided-mcp diff --git a/.github/workflows/mcp-registry-publish.yml b/.github/workflows/mcp-registry-publish.yml new file mode 100644 index 00000000..7d6dfbd8 --- /dev/null +++ b/.github/workflows/mcp-registry-publish.yml @@ -0,0 +1,59 @@ +name: Publish to MCP Registry + +on: + workflow_dispatch: + inputs: + version: + description: Exact released version to register + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: mcp-registry + steps: + - uses: actions/checkout@v5 + - name: Verify requested and packaged versions + env: + REQUESTED_VERSION: ${{ inputs.version }} + run: | + WORKSPACE_VERSION="$(cargo metadata --manifest-path rust/Cargo.toml --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "decided-mcp") | .version')" + SERVER_VERSION="$(jq -r '.version' server.json)" + PACKAGE_VERSION="$(jq -r '.packages[] | select(.identifier == "decided-mcp") | .version' server.json)" + test "$REQUESTED_VERSION" = "$WORKSPACE_VERSION" + test "$REQUESTED_VERSION" = "$SERVER_VERSION" + test "$REQUESTED_VERSION" = "$PACKAGE_VERSION" + - name: Verify MCP crate is available + env: + REQUESTED_VERSION: ${{ inputs.version }} + run: | + for attempt in {1..30}; do + if cargo info "decided-mcp@$REQUESTED_VERSION" --registry crates-io >/dev/null 2>&1; then + exit 0 + fi + sleep 10 + done + echo "::error::decided-mcp $REQUESTED_VERSION did not reach the crates.io index" + exit 1 + - name: Install checksum-pinned MCP publisher + env: + PUBLISHER_VERSION: 1.8.0 + PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf + run: | + ARCHIVE="mcp-publisher_linux_amd64.tar.gz" + curl --fail --location --silent --show-error \ + --output "$ARCHIVE" \ + "https://github.com/modelcontextprotocol/registry/releases/download/v${PUBLISHER_VERSION}/${ARCHIVE}" + echo "${PUBLISHER_SHA256} ${ARCHIVE}" | sha256sum --check + tar -xzf "$ARCHIVE" mcp-publisher + - name: Validate Registry metadata + run: ./mcp-publisher validate + - name: Authenticate with GitHub OIDC + run: ./mcp-publisher login github-oidc + - name: Publish Registry metadata + run: ./mcp-publisher publish diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 0873f838..dfdbd275 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -121,3 +121,24 @@ jobs: exit "$EXIT_CODE" fi echo "AsDecided Sentry passed." + + mcp-distribution: + name: mcp distribution metadata + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Verify Registry and Cargo metadata agree + run: | + WORKSPACE_VERSION="$(cargo metadata --manifest-path rust/Cargo.toml --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "decided-mcp") | .version')" + test "$(jq -r '.name' server.json)" = "io.github.asdecided/core" + test "$(jq -r '.packages | length' server.json)" = "1" + test "$(jq -r '.packages[0].registryType' server.json)" = "cargo" + test "$(jq -r '.packages[0].registryBaseUrl' server.json)" = "https://crates.io" + test "$(jq -r '.packages[0].identifier' server.json)" = "decided-mcp" + test "$(jq -r '.packages[0].transport.type' server.json)" = "stdio" + test "$(jq -r '.version' server.json)" = "$WORKSPACE_VERSION" + test "$(jq -r '.packages[0].version' server.json)" = "$WORKSPACE_VERSION" + grep -Fx "mcp-name: io.github.asdecided/core" rust/decided-mcp/README.md + - name: Package MCP crate + working-directory: rust + run: cargo publish --dry-run --locked -p decided-mcp diff --git a/decisions/decisions/adr-039-lore-server-identity.md b/decisions/decisions/adr-039-lore-server-identity.md index f24afea0..1250d31b 100644 --- a/decisions/decisions/adr-039-lore-server-identity.md +++ b/decisions/decisions/adr-039-lore-server-identity.md @@ -5,6 +5,10 @@ type: decision --- # ADR-039: Lore Server Identity +> The public Registry identity in this historical decision is superseded by +> ADR-124. Its `lore` handshake identity remains frozen only for the legacy MCP +> compatibility path specified by ADR-121. + ## Status Accepted diff --git a/decisions/decisions/adr-124-public-mcp-distribution.md b/decisions/decisions/adr-124-public-mcp-distribution.md new file mode 100644 index 00000000..2d8e3340 --- /dev/null +++ b/decisions/decisions/adr-124-public-mcp-distribution.md @@ -0,0 +1,137 @@ +--- +schema_version: 1 +id: RAC-KYVTHFQD44BP +type: decision +--- +# ADR-124: Publish the Native MCP Server Through Cargo and the Official Registry + +## Context + +AsDecided already ships `decided-mcp` in Homebrew and native GitHub Release +archives, but those channels do not make the server discoverable to MCP clients. +The official MCP Registry is a metadata registry: a listing must point at an +independently published package and prove control of both its namespace and that +package. Its live validator supports Cargo packages and verifies ownership by +reading an exact `mcp-name` token from the crate README. + +ADR-039 reserved `io.github.tcballard/lore` before the AsDecided rename and +before the Rust cutover. ADR-121 has since made `decided-mcp` the current +protocol identity while retaining `lore` only in frozen legacy responses. A +fresh public distribution identity is therefore required; reviving the retired +name would make discovery disagree with the product, repository, and current +wire protocol. + +## Decision + +The public MCP Registry identity is `io.github.asdecided/core`. + +- The native server is published as the dedicated `decided-mcp` crate on + crates.io. It is not hidden inside the `decided` CLI package and is not + wrapped in Python, npm, or an OCI-only launcher. +- The crate README carries the exact ownership marker + `mcp-name: io.github.asdecided/core`. Registry namespace ownership is proven + independently through GitHub OIDC for the `asdecided` organisation. +- `server.json` identifies the package as Cargo, pins an exact release version, + and declares stdio transport. Its title is AsDecided; its executable and + current MCP handshake identity remain `decided-mcp`. +- Registry publication happens only after the exact `decided-mcp` version is + visible on crates.io. The publisher binary is version- and checksum-pinned, + validates the manifest before publishing, and uses no long-lived Registry + credential. +- The listing describes a local server over a local repository. It does not + imply that AsDecided hosts, uploads, or remotely indexes a user's corpus. +- The first Registry publication must come from a new immutable release commit + and tag. Existing v0.26.0 artifacts are not retroactively rebuilt. + +This decision supersedes only ADR-039's public Registry identity. ADR-121's +legacy `lore` bytes remain unchanged. + +## Consequences + +### Positive + +- MCP clients and catalogues can discover the canonical Rust server through the + ecosystem's official metadata surface. +- Cargo is both the installation source and the Registry verification source, + so there is no second runtime or repackaged implementation to drift. +- GitHub OIDC and a checksum-pinned publisher preserve the release chain without + introducing a reusable organisation-wide Registry token. + +### Negative + +- A release now has another ordered publication step and can be partially + complete if crates.io succeeds but Registry publication fails. +- `server.json`, the workspace, and the package record must carry exactly the + same version at each release. +- The Registry is in preview, so its schema or availability can change. + +### Risks + +- A Registry retry could accidentally rebuild an already-published crate. + Mitigation: crate publication and Registry publication are separate manually + dispatched workflows; the latter first verifies that the immutable crate is + already available. +- Namespace or package verification could silently weaken. Mitigation: pin the + publisher checksum and test the exact namespace, package, transport, version, + and README token in pull-request CI. + +## Status + +Accepted + +## Category + +Architecture + +## Alternatives Considered + +### Register the GitHub Release archive or existing OCI image + +Rejected as the primary Registry package. Cargo is natively supported and maps +directly to the Rust executable, while archive installation is not a Registry +package type and the existing image defaults to the CLI rather than the MCP +server. + +### Publish an npm or Python wrapper + +Rejected. A wrapper would restore the duplicate-runtime maintenance burden the +Rust cutover removed and would add no product capability. + +### Keep `io.github.tcballard/lore` + +Rejected. It represents a retired owner and product name and is retained only +where ADR-121 requires historical wire compatibility. + +## Code Constraints + +```yaml +version: 1 +eligibility: eligible +reason: "The public identity and publication path have stable source anchors." +rules: + - id: registry-keeps-canonical-name + kind: require_pattern + path_glob: "server.json" + pattern: '"name": "io.github.asdecided/core"' + message: "The MCP Registry manifest must retain the canonical AsDecided identity." + - id: crate-keeps-registry-ownership-token + kind: require_pattern + path_glob: "rust/decided-mcp/README.md" + pattern: 'mcp-name: io.github.asdecided/core' + message: "The published crate must retain the Registry ownership token." + - id: registry-publish-keeps-oidc + kind: require_pattern + path_glob: ".github/workflows/mcp-registry-publish.yml" + pattern: 'login github-oidc' + message: "Registry publication must authenticate without a long-lived token." +``` + +## Related Decisions + +- adr-039 +- adr-111 +- adr-121 + +## Related Requirements + +- asdecided-public-mcp-distribution diff --git a/decisions/designs/mcp-registry-cargo-publishing.md b/decisions/designs/mcp-registry-cargo-publishing.md new file mode 100644 index 00000000..309afb68 --- /dev/null +++ b/decisions/designs/mcp-registry-cargo-publishing.md @@ -0,0 +1,97 @@ +--- +schema_version: 1 +id: RAC-KYVTHR4WNT8T +type: design +--- +# MCP Registry Cargo Publishing + +## Context + +ADR-124 selects Cargo as the package authority behind AsDecided's official MCP +Registry record. The release design must coordinate three immutable values—the +workspace version, crates.io version, and Registry version—without making a +Registry outage force a duplicate crate publication. + +## User Need + +An MCP user needs to find the canonical AsDecided server, understand that it +runs locally over their repository, and install the actual Rust implementation. +A maintainer needs a release path that is inspectable, secretless at the +Registry boundary, and safe to retry after partial external failure. + +## Design + +The repository contains four connected surfaces: + +1. `rust/decided-mcp/Cargo.toml` defines a publishable crates.io package whose + README contains `mcp-name: io.github.asdecided/core`. +2. `server.json` points `io.github.asdecided/core` at that exact Cargo package + version using stdio transport. +3. The crates.io workflow publishes `asdecided-core`, waits for it to reach the + index, then publishes both executable crates, `decided` and `decided-mcp`. +4. A separate Registry workflow accepts an exact version, proves it matches the + workspace and both manifest version fields, waits for that crate on + crates.io, installs an exact checksum-pinned `mcp-publisher`, validates the + listing, authenticates with GitHub OIDC, and publishes. + +The first publication is made in the next patch release. v0.26.0 is not rebuilt +after its immutable release tag. Each subsequent release updates the workspace, +exact engine dependencies, lockfile, changelog, and both `server.json` version +fields together before the tag is created. + +## Constraints + +- Registry metadata cannot publish before its package exists and carries the + ownership token. +- crates.io versions and Git tags are immutable; recovery must move forward or + retry metadata publication, never overwrite an artifact. +- The Registry is metadata-only and in preview. Local installation and use must + remain viable if it is unavailable. +- Current identity is `decided-mcp`; ADR-121's legacy `lore` response bytes must + not change. + +## Rationale + +A dedicated Cargo package makes the Registry record resolve directly to the +binary users run. Splitting Registry publication from crate publication gives +the non-transactional release chain a safe recovery point: once crates.io has +accepted a version, the Registry step can be retried without touching it. + +## Alternatives + +- **One monolithic workflow:** rejected because a Registry failure leaves no + safe way to rerun after crates.io has accepted immutable versions. +- **OCI-only package:** rejected because the current image launches the CLI by + default and Cargo is the native distribution surface for this Rust server. +- **Hosted remote entry:** rejected because AsDecided has not decided to host + user corpora or introduce the associated tenancy, authentication, and trust + boundary. + +## Accessibility + +The title and description use plain product language rather than internal +component names. Installation and configuration examples remain text, are +copyable, and do not depend on images or colour. + +## Style Guidance + +Describe capability and authority precisely: “read-only,” “local repository,” +and “deterministic” are useful; “hosted,” “AI-powered,” or universal client +compatibility are unsupported claims. Keep AsDecided as the display name and +reserve `decided-mcp` for the executable and package. + +## Open Questions + +- Which MCP clients will consume Cargo package records automatically while the + Registry remains in preview? +- When the Registry reaches general availability, should publication move from + manual dispatch to an automatically gated release job? + +## Related Decisions + +- adr-124 +- adr-121 + +## Related Requirements + +- asdecided-public-mcp-distribution diff --git a/decisions/requirements/asdecided-public-mcp-distribution.md b/decisions/requirements/asdecided-public-mcp-distribution.md new file mode 100644 index 00000000..40a1b5ec --- /dev/null +++ b/decisions/requirements/asdecided-public-mcp-distribution.md @@ -0,0 +1,78 @@ +--- +schema_version: 1 +id: RAC-KYVTHR3TXXHC +type: requirement +--- +# REQ-AsDecided-Public-MCP-Distribution + +> The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are +> to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only +> when, they appear in all capitals. + +## Status + +Accepted + +## Problem + +Installing AsDecided through Homebrew or a GitHub Release does not make its MCP +server discoverable through MCP-native catalogues. A public listing must be +verifiably controlled by AsDecided, resolve to the native Rust server, preserve +the local-first trust boundary, and participate in the same immutable release +line as every other distribution channel. + +## Requirements + +- [REQ-001] `decided-mcp` MUST be published as an installable crates.io package from the same release commit and exact version as the AsDecided workspace. +- [REQ-002] The official MCP Registry record MUST use the canonical name `io.github.asdecided/core`, title `AsDecided`, Cargo package identifier `decided-mcp`, and stdio transport. +- [REQ-003] The published crate README MUST contain the exact ownership token `mcp-name: io.github.asdecided/core`, and Registry authentication MUST use GitHub OIDC rather than a long-lived personal or organisation token. +- [REQ-004] The Registry version, package version, and workspace version MUST be equal. Publication MUST fail before Registry mutation when they disagree. +- [REQ-005] Registry publication MUST wait until the exact crate version is retrievable from crates.io, then run the checksum-pinned official publisher's validation before publishing. +- [REQ-006] Registry publication MUST be independently retryable without republishing or overwriting an immutable crate version. +- [REQ-007] The public metadata MUST describe AsDecided as a local, read-only MCP server and MUST NOT imply a hosted corpus, remote index, model call, or write authority. + +## Acceptance Criteria + +- `cargo publish --dry-run --locked -p decided-mcp` succeeds from a clean release + candidate. +- Pull-request CI proves the manifest namespace, Cargo registry, package, + transport, and versions and finds the exact ownership token in the packaged + README. +- On a release, crates.io serves the exact `decided-mcp` version before + `mcp-publisher validate` and `mcp-publisher publish` run. +- Re-running only the Registry workflow can recover from a Registry outage + without attempting to publish any crate again. +- Installing the listed crate exposes `decided-mcp`, which serves a local corpus + over stdio with the same current protocol identity tested by ADR-121. + +## Success Metrics + +- The current AsDecided release is discoverable under exactly one canonical + official MCP Registry identity. +- Registry metadata and crates.io never advertise different AsDecided versions. +- Registry publication requires no persistent Registry secret. + +## Risks + +- The preview Registry may reset data or change schema. The crate remains a + complete independent installation channel, and the pinned workflow makes a + republish explicit. +- Some clients may not yet install Cargo packages automatically. Homebrew and + native archives remain documented alternatives; the listing must not claim + universal client support. + +## Assumptions + +- The official Registry continues to accept Cargo packages and GitHub + organisation OIDC namespaces. +- crates.io remains the canonical Rust package registry for the native server. + +## Related Decisions + +- adr-124 +- adr-121 +- adr-039 + +## Related Requirements + +- rac-release-versioning diff --git a/rust/decided-mcp/Cargo.toml b/rust/decided-mcp/Cargo.toml index cf7e25b5..894391c3 100644 --- a/rust/decided-mcp/Cargo.toml +++ b/rust/decided-mcp/Cargo.toml @@ -5,9 +5,13 @@ edition.workspace = true license.workspace = true repository.workspace = true homepage.workspace = true -publish = false +description = "Read-only MCP access to deterministic engineering decisions" +readme = "README.md" +keywords = ["mcp", "decisions", "architecture", "agents", "offline"] +categories = ["command-line-utilities", "development-tools"] +publish = ["crates-io"] [dependencies] -rac-engine = { package = "asdecided-core", path = "../rac-engine" } +rac-engine = { package = "asdecided-core", version = "=0.26.0", path = "../rac-engine" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } diff --git a/rust/decided-mcp/README.md b/rust/decided-mcp/README.md new file mode 100644 index 00000000..ed244fbb --- /dev/null +++ b/rust/decided-mcp/README.md @@ -0,0 +1,29 @@ +# decided-mcp + +`decided-mcp` gives agents read-only access to the engineering decisions stored +in an AsDecided repository. It runs locally and deterministically: no hosted +index, embeddings, model call, or Python runtime is required. + +```sh +cargo install decided-mcp +decided-mcp --root /path/to/repository +``` + +Example client configuration: + +```json +{ + "mcpServers": { + "asdecided": { + "command": "decided-mcp", + "args": ["--root", "."] + } + } +} +``` + +The server supports MCP over stdio by default and reads the corpus from the +current directory unless `--root` is supplied. Documentation and source: +[`asdecided/core`](https://github.com/asdecided/core). + +mcp-name: io.github.asdecided/core diff --git a/server.json b/server.json index 4d0e217b..ad50aa93 100644 --- a/server.json +++ b/server.json @@ -1,17 +1,20 @@ { - "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", - "name": "io.github.tcballard/lore", - "description": "Serve your repo's recorded product knowledge - requirements, decisions (ADRs), designs, roadmaps - to coding agents. Deterministic, read-only.", + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.asdecided/core", + "description": "Read-only access to deterministic engineering decisions stored in a local repository.", + "title": "AsDecided", "repository": { "url": "https://github.com/asdecided/core", "source": "github" }, - "version": "0.11.0", + "version": "0.26.0", + "websiteUrl": "https://asdecided.com", "packages": [ { - "registryType": "pypi", - "identifier": "asdecided-core", - "version": "0.11.0", + "registryType": "cargo", + "registryBaseUrl": "https://crates.io", + "identifier": "decided-mcp", + "version": "0.26.0", "transport": { "type": "stdio" }