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
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ permissions:
jobs:
sonarqube:
name: SonarQube
# Opt-in via repository variable SONAR_ENABLED=true (matching
# boj-server-cartridges) so missing/unrotated SONAR_TOKEN does not produce
# a permanent false-red check on PRs and main.
if: vars.SONAR_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4.3.1
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/container-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
--label "org.opencontainers.image.description=Bundle of Joy MCP Server — 112 cartridges (111 Zig FFI + 1 JS)" \
--label "org.opencontainers.image.licenses=MPL-2.0" \
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
--label "io.modelcontextprotocol.server.name=io.github.hyperpolymath/boj-server" \
.

- name: Push to GitHub Container Registry
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ permissions:

jobs:
governance:
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@5a0891e9e58b785181a92e141505ca96ca987b42
# Previous 5a0891e9 pin predates hyperpolymath/standards#1271 (f335165),
# which fixed actions.lock leaf tag refs so runner job setup succeeds,
# and hyperpolymath/standards#1260 (UUID v8 conformance).
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@e16a23170fa378bf3b1444c3ee94100413ae3ff7
secrets:
HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }}
11 changes: 9 additions & 2 deletions .github/workflows/mirror.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,12 @@ concurrency:

jobs:
mirror:
uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a
secrets: inherit
uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@e16a23170fa378bf3b1444c3ee94100413ae3ff7
secrets:
GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }}
BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }}
CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }}
SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }}
DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }}
GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }}
RADICLE_KEY: ${{ secrets.RADICLE_KEY }}
47 changes: 43 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# This workflow is managed by gh actions-lock.
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Publish workflow — publishes to npm and JSR on version tag push (v*).
# Requires NPM_TOKEN and JSR_TOKEN repository secrets.
# Publish workflow — publishes to npm, the official MCP Server Registry, and JSR on version tag push (v*).
# Requires NPM_TOKEN and JSR_TOKEN repository secrets; MCP Registry uses GitHub OIDC (zero secrets).
name: Publish

on:
Expand All @@ -19,7 +19,7 @@ permissions:

jobs:
publish-npm:
name: Publish to npm
name: Publish to npm & MCP Registry
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
Expand All @@ -34,20 +34,59 @@ jobs:
node-version: '22'
registry-url: 'https://registry.npmjs.org'

- name: Verify package version matches tag
- name: Verify package and server.json versions match tag
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
MCP_NAME=$(node -p "require('./package.json').mcpName")
SRV_VERSION=$(node -p "require('./server.json').version")
SRV_NAME=$(node -p "require('./server.json').name")
SRV_PKG_ID=$(node -p "require('./server.json').packages?.[0]?.identifier ?? ''")
SRV_PKG_VERSION=$(node -p "require('./server.json').packages?.[0]?.version ?? ''")
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
exit 1
fi
if [ "$TAG_VERSION" != "$SRV_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match server.json version ($SRV_VERSION)"
exit 1
fi
if [ "$MCP_NAME" != "$SRV_NAME" ]; then
echo "::error::package.json mcpName ($MCP_NAME) does not match server.json name ($SRV_NAME)"
exit 1
fi
if [ "$PKG_NAME" != "$SRV_PKG_ID" ]; then
echo "::error::package.json name ($PKG_NAME) does not match server.json packages[0].identifier ($SRV_PKG_ID)"
exit 1
fi
if [ "$TAG_VERSION" != "$SRV_PKG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match server.json packages[0].version ($SRV_PKG_VERSION)"
exit 1
fi

- name: Install mcp-publisher
run: |
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
curl -fsSL -o /tmp/mcp-publisher.tar.gz \
"https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${OS}_${ARCH}.tar.gz"
tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp mcp-publisher
chmod +x /tmp/mcp-publisher

- name: Validate MCP manifest
run: /tmp/mcp-publisher validate server.json
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Authenticate and publish to MCP Registry
run: |
/tmp/mcp-publisher login github-oidc
/tmp/mcp-publisher publish

publish-jsr:
name: Publish to JSR
runs-on: ubuntu-latest
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ permissions:
jobs:
analysis:
permissions:
# `contents: read` is REQUIRED, not redundant. Declaring `permissions:`
# at job level sets every unlisted scope to `none`, and a called
# workflow can never hold more than the calling job grants it. The
# reusable declares top-level `permissions: contents: read` and its
# first step is actions/checkout, so omitting `contents` here handed
# it `contents: none` and the call died at startup with zero jobs.
# The green sibling (boj-server-cartridges) lists all three.
# `actions: read` and `contents: read` are REQUIRED, not redundant.
# Declaring `permissions:` at job level sets every unlisted scope to
# `none`, and a called workflow can never hold more than the calling job
# grants it. `scorecard-reusable.yml` requests `actions: read` (for
# Scorecard's Packaging check), `contents: read`, `security-events: write`,
# and `id-token: write` on its `scorecard` job.
actions: read
contents: read
security-events: write
id-token: write
uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a
uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@e16a23170fa378bf3b1444c3ee94100413ae3ff7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
6 changes: 4 additions & 2 deletions .github/workflows/secret-scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ jobs:
scan:
permissions:
contents: read
uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a
secrets: inherit
# Previous 84355587 pin predates hyperpolymath/standards#1271 (f335165),
# which fixed actions.lock leaf tag refs so runner job setup succeeds.
# Also drops `secrets: inherit` per hyperpolymath/standards#1283 (e16a231).
uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@e16a23170fa378bf3b1444c3ee94100413ae3ff7
1 change: 1 addition & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
zig = "0.16.0"
elixir = "1.18.4-otp-25"
idris2 = "0.8.0"
bun = "1.4.2"
deno = "2.7.14"
5 changes: 4 additions & 1 deletion Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

FROM cgr.dev/chainguard/node:latest

COPY --from=oven/bun:1.4.2 /usr/local/bin/bun /usr/local/bin/bun

LABEL org.opencontainers.image.title="Bundle of Joy Server (MCP Bridge)" \
org.opencontainers.image.description="MCP stdio transport for BoJ cartridge toolkit" \
org.opencontainers.image.url="https://github.com/hyperpolymath/boj-server" \
org.opencontainers.image.source="https://github.com/hyperpolymath/boj-server" \
org.opencontainers.image.vendor="hyperpolymath" \
org.opencontainers.image.licenses="MPL-2.0" \
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
io.modelcontextprotocol.server.name="io.github.hyperpolymath/boj-server"

WORKDIR /app

Expand Down
1 change: 1 addition & 0 deletions container/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ LABEL org.opencontainers.image.title="Bundle of Joy Server" \
org.opencontainers.image.vendor="hyperpolymath" \
org.opencontainers.image.licenses="MPL-2.0" \
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
io.modelcontextprotocol.server.name="io.github.hyperpolymath/boj-server" \
dev.stapeln.compose="container/compose.toml"

# Minimal runtime deps: OpenSSL (Elixir TLS), ncurses (Erlang), glibc (boj-invoke + .so)
Expand Down
3 changes: 2 additions & 1 deletion container/Containerfile.fly
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ LABEL org.opencontainers.image.title="Bundle of Joy Server" \
org.opencontainers.image.source="https://github.com/hyperpolymath/boj-server" \
org.opencontainers.image.vendor="hyperpolymath" \
org.opencontainers.image.licenses="MPL-2.0" \
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
io.modelcontextprotocol.server.name="io.github.hyperpolymath/boj-server"

# wolfi is glibc-native: no gcompat; ncurses not ncurses-libs; add curl for healthcheck
RUN apk add --no-cache ca-certificates openssl ncurses libstdc++ libssl3 curl
Expand Down
7 changes: 4 additions & 3 deletions container/Containerfile.mcp-bridge
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@

FROM cgr.dev/chainguard/wolfi-base:latest

# Install Deno runtime (replaces banned Node.js/npm)
RUN apk add --no-cache deno
# Install Bun and Deno runtimes (preference order: Bun > Deno per LANGUAGE-POLICY)
RUN apk add --no-cache bun deno

LABEL org.opencontainers.image.title="Bundle of Joy Server (MCP Bridge)" \
org.opencontainers.image.description="MCP stdio transport for BoJ cartridge toolkit" \
org.opencontainers.image.url="https://github.com/hyperpolymath/boj-server" \
org.opencontainers.image.source="https://github.com/hyperpolymath/boj-server" \
org.opencontainers.image.vendor="hyperpolymath" \
org.opencontainers.image.licenses="MPL-2.0" \
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
io.modelcontextprotocol.server.name="io.github.hyperpolymath/boj-server"

WORKDIR /app

Expand Down
36 changes: 36 additions & 0 deletions mcp-bridge/tests/dispatch_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,39 @@ test("otel enabled-mode produces well-formed span handles", async () => {
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
}
});

// -----------------------------------------------------------------
// 16. MCP Registry (server.json) and package.json coherence
// -----------------------------------------------------------------
test("server.json and package.json satisfy MCP Registry constraints", () => {
const repoRoot = join(__dirname, "../..");
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
const jsr = JSON.parse(readFileSync(join(repoRoot, "jsr.json"), "utf8"));
const srv = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"));

assert.equal(
srv.$schema,
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
);
assert.equal(srv.name, "io.github.hyperpolymath/boj-server");
assert.equal(pkg.mcpName, srv.name, "package.json mcpName must match server.json name");
assert.equal(srv.version, pkg.version, "server.json version must match package.json version");
assert.equal(srv.version, jsr.version, "server.json version must match jsr.json version");
assert.ok(
srv.description && srv.description.length >= 1 && srv.description.length <= 100,
`server.json description must be 1..100 chars (got ${srv.description?.length})`,
);
assert.equal(srv.repository?.source, "github");
assert.match(
srv.repository?.url ?? "",
/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+$/,
"repository.url must match MCP Registry githubURLRegex (no .git suffix)",
);
assert.ok(Array.isArray(srv.packages) && srv.packages.length >= 1);
const npmPkg = srv.packages.find((p) => p.registryType === "npm");
assert.ok(npmPkg, "server.json must declare an npm package entry");
assert.equal(npmPkg.identifier, pkg.name);
assert.equal(npmPkg.version, pkg.version);
assert.equal(npmPkg.transport?.type, "stdio");
});

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "@hyperpolymath/boj-server",
"version": "0.5.0",
"mcpName": "io.github.hyperpolymath/boj-server",
"description": "Bundle of Joy (BoJ) MCP Server — cartridge-based DevOps + multi-agent coordination toolkit with 125 domain cartridges (database, container, git, k8s, observability, secrets, IaC, multi-instance AI coord, and more). Formally verified (Idris2) core; MPL-2.0.",
"license": "MPL-2.0",
"author": "Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>",
"files": [
"mcp-bridge/",
"server.json",
"README.md",
"LICENSE",
"NOTICE"
Expand Down
103 changes: 103 additions & 0 deletions server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.hyperpolymath/boj-server",
"title": "Bundle of Joy (BoJ) Server",
"description": "Cartridge-based MCP server for DevOps, cloud, database, k8s, and formally verified coordination.",
"repository": {
"url": "https://github.com/hyperpolymath/boj-server",
"source": "github"
},
"websiteUrl": "https://github.com/hyperpolymath/boj-server",
"version": "0.5.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "@hyperpolymath/boj-server",
"version": "0.5.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
},
"environmentVariables": [
{
"name": "BOJ_URL",
"description": "Base URL for the BoJ REST API backend (loads cartridges from boj-server-cartridges).",
"default": "http://localhost:7700",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "BOJ_TOOL_SCOPE",
"description": "Advertised cartridge tool-surface scope ('full', 'core', or CSV of domains like 'core,github,browser').",
"default": "full",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "BOJ_PROTOCOL",
"description": "Primary protocol for cartridge communication (rest, grpc, graphql).",
"default": "rest",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "BOJ_RATE_LIMIT",
"description": "Maximum number of tool calls permitted per minute.",
"default": "60",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "BOJ_LOG_LEVEL",
"description": "Logging verbosity (debug, info, warn, error, silent).",
"default": "info",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "COORD_BACKEND_URL",
"description": "URL for the local multi-agent coordination bus backend.",
"default": "http://127.0.0.1:7745",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "GITHUB_TOKEN",
"description": "Personal Access Token for GitHub API operations (required for boj_github_* cartridge tools).",
"format": "string",
"isRequired": false,
"isSecret": true
},
{
"name": "GITLAB_TOKEN",
"description": "Private Token for GitLab API operations (required for boj_gitlab_* cartridge tools).",
"format": "string",
"isRequired": false,
"isSecret": true
},
{
"name": "GITLAB_URL",
"description": "Base URL for the GitLab instance.",
"default": "https://gitlab.com",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "OTEL_EXPORTER_OTLP_ENDPOINT",
"description": "Optional OTLP/HTTP collector endpoint (e.g. http://localhost:4318) for tool-call trace export.",
"format": "string",
"isRequired": false,
"isSecret": false
}
]
}
]
}
Loading