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
89 changes: 89 additions & 0 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
5 changes: 3 additions & 2 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
40 changes: 40 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions npm/bin/oxcode-mcp.js
Original file line number Diff line number Diff line change
@@ -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));
37 changes: 37 additions & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
34 changes: 34 additions & 0 deletions server.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
]
}