diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..198f0bd --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,89 @@ +# Custom dist publish job: publishes the npm launcher (@snowmead/oxcode-mcp) and +# registers the server in the official MCP Registry (io.github.snowmead/oxcode). +# dist invokes this during the release's publish phase (see `publish-jobs` in the +# dist config), passing the plan and inheriting repo secrets — so it runs on the +# same `v*` tag that builds binaries. The npm + server.json versions are stamped +# from the oxcode-cli crate version so they can never drift from the release. +# +# npm auth is OIDC "trusted publishing" — no NPM_TOKEN. Because the publish runs +# inside THIS reusable workflow, npm validates the CALLER's filename, so the npm +# Trusted Publisher must be configured with workflow `release.yml` (org oxgraph, +# repo oxcode). id-token: write is required in both release.yml's job and here, +# and it needs npm >= 11.5.1 / Node >= 22.14 (we bump npm below). +# +# The MCP Registry publish still needs one secret: +# MCP_GITHUB_TOKEN — GitHub PAT for user `snowmead` (read:org, read:user); +# a user PAT grants the io.github.snowmead/* namespace even +# though this repo lives under the oxgraph org (OIDC would +# auth as oxgraph, so PAT — not github-oidc — is required). +name: publish-npm + +on: + workflow_call: + inputs: + plan: + required: true + type: string + +jobs: + publish-npm: + runs-on: ubuntu-latest + # id-token: write is required in BOTH the caller (release.yml) and here for + # npm OIDC trusted publishing. + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4.2.2 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + + # OIDC trusted publishing needs npm >= 11.5.1 (newer than the npm bundled + # with Node 24). + - name: Upgrade npm for OIDC publishing + run: npm install -g npm@latest + + # Stamp the release version (from the oxcode-cli crate — the published + # binary) into both the npm package and the registry manifest. + - name: Stamp release version + run: | + set -euo pipefail + VERSION="$(grep -m1 '^version' crates/oxcode-cli/Cargo.toml | cut -d'"' -f2)" + echo "Publishing @snowmead/oxcode-mcp and io.github.snowmead/oxcode at $VERSION" + ( cd npm && npm version "$VERSION" --no-git-tag-version --allow-same-version ) + jq --arg v "$VERSION" '.version = $v | .packages[0].version = $v' server.json > server.json.tmp + mv server.json.tmp server.json + + # Must publish to npm BEFORE the registry publish: the registry validates + # the live package and its `mcpName` field. OIDC trusted publishing — no + # token; provenance is auto-generated since the repo and package are public. + - name: Publish @snowmead/oxcode-mcp to npm (OIDC trusted publishing) + run: npm publish ./npm --access public + + - name: Install mcp-publisher + run: | + set -euo pipefail + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + + - name: Authenticate to MCP Registry (GitHub PAT) + env: + MCP_GITHUB_TOKEN: ${{ secrets.MCP_GITHUB_TOKEN }} + run: ./mcp-publisher login github --token "$MCP_GITHUB_TOKEN" + + # npm's CDN can lag a few seconds behind publish, and the registry + # validates the live package, so retry a handful of times. + - name: Publish to MCP Registry + run: | + set -euo pipefail + for attempt in 1 2 3 4 5; do + if ./mcp-publisher publish; then + exit 0 + fi + echo "publish attempt $attempt failed; waiting for npm propagation..." >&2 + sleep 30 + done + echo "mcp-publisher publish failed after retries" >&2 + exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7327c7..c798e55 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -292,15 +292,30 @@ jobs: "id-token": "write" "packages": "write" + custom-publish-npm: + needs: + - plan + - host + if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} + uses: ./.github/workflows/publish-npm.yml + with: + plan: ${{ needs.plan.outputs.val }} + secrets: inherit + # publish jobs get escalated permissions + permissions: + "id-token": "write" + "packages": "write" + announce: needs: - plan - host - custom-publish-crates + - custom-publish-npm # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! - if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') }} + if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') && (needs.custom-publish-npm.result == 'skipped' || needs.custom-publish-npm.result == 'success') }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/dist-workspace.toml b/dist-workspace.toml index 692072b..e061d97 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -17,7 +17,8 @@ targets = [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", ] -# Publish the workspace crates to crates.io on the same tag (custom job). -publish-jobs = ["./publish-crates"] +# Publish the workspace crates to crates.io and the npm launcher + MCP Registry +# entry on the same tag (custom jobs). +publish-jobs = ["./publish-crates", "./publish-npm"] # Only run the cheap "plan" check on PRs; full builds on tags. pr-run-mode = "plan" diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..c33264f --- /dev/null +++ b/npm/README.md @@ -0,0 +1,40 @@ +# @snowmead/oxcode-mcp + +npm launcher for the **oxcode MCP server** — PageRank-curated code intelligence +for coding agents over an indexed repository. + +This package is a thin wrapper: it runs `oxcode mcp` from the `oxcode` binary, +which you install once and which then self-updates. The package exists so the +server can be listed in the +[official MCP Registry](https://registry.modelcontextprotocol.io); it does +**not** bundle or download the binary. + +## Prerequisite: install the `oxcode` binary + +```sh +# Prebuilt (recommended) +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/oxgraph/oxcode/releases/latest/download/oxcode-cli-installer.sh | sh +# or +cargo binstall oxcode-cli # prebuilt, no compile +cargo install oxcode-cli # from source +``` + +(The crate is `oxcode-cli`; the command is `oxcode`.) + +## Use as an MCP server + +```json +{ + "mcpServers": { + "oxcode": { "command": "npx", "args": ["-y", "@snowmead/oxcode-mcp"] } + } +} +``` + +This is equivalent to running `oxcode mcp` directly — if you already have the +binary, pointing your client at `command: "oxcode", args: ["mcp"]` is simpler and +avoids the Node hop. + +Index a project first (`oxcode index`, or call the `oxcode_index` tool) so the +read tools have data. See the [oxcode repo](https://github.com/oxgraph/oxcode) +for the full CLI and tool list. diff --git a/npm/bin/oxcode-mcp.js b/npm/bin/oxcode-mcp.js new file mode 100755 index 0000000..8009fc7 --- /dev/null +++ b/npm/bin/oxcode-mcp.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node +"use strict"; + +// Thin launcher: the real MCP server is `oxcode mcp` from the self-updating +// oxcode binary. This wrapper exists only so the server can be referenced by +// the official MCP Registry (which requires a validated npm package). It +// deliberately does not download or manage the binary — `oxcode` owns its own +// updates. If `oxcode` is on PATH this is equivalent to running `oxcode mcp`. + +const { spawnSync } = require("node:child_process"); + +const result = spawnSync("oxcode", ["mcp", ...process.argv.slice(2)], { + stdio: "inherit", +}); + +if (result.error && result.error.code === "ENOENT") { + process.stderr.write( + "oxcode not found on PATH. Install it once (it self-updates after that):\n" + + " curl --proto '=https' --tlsv1.2 -LsSf https://github.com/oxgraph/oxcode/releases/latest/download/oxcode-cli-installer.sh | sh\n" + + " # or: cargo binstall oxcode-cli (prebuilt, no compile)\n" + + " # or: cargo install oxcode-cli (from source)\n", + ); + process.exit(127); +} + +if (result.error) { + process.stderr.write(`failed to launch oxcode: ${result.error.message}\n`); + process.exit(1); +} + +// A signal-terminated child has a null status; surface it as a generic failure. +process.exit(result.signal ? 1 : (result.status ?? 1)); diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..61f4a53 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,37 @@ +{ + "name": "@snowmead/oxcode-mcp", + "version": "0.2.0", + "description": "npm launcher for the oxcode MCP server — PageRank-curated code intelligence for coding agents. Requires the self-updating `oxcode` binary on PATH.", + "mcpName": "io.github.snowmead/oxcode", + "type": "commonjs", + "bin": { + "oxcode-mcp": "bin/oxcode-mcp.js" + }, + "files": [ + "bin/", + "README.md" + ], + "engines": { + "node": ">=18" + }, + "author": { + "name": "oxgraph", + "email": "michael@snowmead.com" + }, + "homepage": "https://oxgraph.io", + "repository": { + "type": "git", + "url": "git+https://github.com/oxgraph/oxcode.git", + "directory": "npm" + }, + "license": "MIT", + "keywords": [ + "mcp", + "model-context-protocol", + "code-intelligence", + "code-navigation", + "pagerank", + "oxgraph", + "oxcode" + ] +} diff --git a/server.json b/server.json new file mode 100644 index 0000000..cc9ab32 --- /dev/null +++ b/server.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.snowmead/oxcode", + "description": "PageRank-curated code intelligence for coding agents over an indexed source repository.", + "repository": { + "url": "https://github.com/oxgraph/oxcode", + "source": "github" + }, + "version": "0.2.0", + "packages": [ + { + "registryType": "npm", + "identifier": "@snowmead/oxcode-mcp", + "version": "0.2.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "OXCODE_NO_AUTO_UPDATE", + "description": "Set to 1 to disable the binary's startup self-update check (CI, offline, or reproducible environments).", + "isRequired": false, + "isSecret": false + }, + { + "name": "GITHUB_TOKEN", + "description": "Optional: used during self-update to avoid unauthenticated GitHub API rate limits.", + "isRequired": false, + "isSecret": true + } + ] + } + ] +}