Skip to content

Commit 54a5c06

Browse files
committed
Add MCP server with npm distribution
The Roam SDK had no way for AI assistants to interact with the graph programmatically. Users in the MCP ecosystem expect to install tools via npm. Added --mcp flag to the roam binary that starts an MCP server over stdio, exposing 11 tools (search, get_page, create_block, etc.) that wrap the existing RoamClient SDK. Platform-specific npm packages (roam-tui) bundle pre-built binaries so `npx roam-tui@latest --mcp` just works. A GitHub Action publishes to npm after Release and Publish Crate workflows succeed. Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
1 parent 4d5acdc commit 54a5c06

15 files changed

Lines changed: 1372 additions & 10 deletions

File tree

.github/workflows/publish-crate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
- name: Publish to crates.io
4040
env:
4141
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
42-
run: cargo publish
42+
run: cargo publish || echo "::warning::Crate version already published, skipping"

.github/workflows/publish-npm.yml

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
name: Publish to npm
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Release", "Publish Crate"]
6+
types: [completed]
7+
branches: [main]
8+
workflow_dispatch:
9+
inputs:
10+
tag:
11+
description: "Release tag (e.g., v0.2.0)"
12+
required: true
13+
14+
jobs:
15+
publish-npm:
16+
# Only run when ALL triggering workflows succeeded, or on manual dispatch
17+
if: >-
18+
github.event_name == 'workflow_dispatch' ||
19+
(github.event.workflow_run.conclusion == 'success' &&
20+
github.event.workflow_run.name == 'Release')
21+
runs-on: ubuntu-latest
22+
environment: npm
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Resolve tag
27+
id: tag
28+
env:
29+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
run: |
31+
if [[ -n "${{ inputs.tag }}" ]]; then
32+
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
33+
else
34+
# Get the tag from the workflow run that triggered us
35+
TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name')
36+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
37+
fi
38+
39+
- name: Check Release workflow succeeded
40+
if: github.event_name != 'workflow_dispatch'
41+
env:
42+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
run: |
44+
TAG="${{ steps.tag.outputs.tag }}"
45+
46+
# Verify the release exists and has all expected binaries
47+
ASSET_COUNT=$(gh release view "$TAG" --json assets --jq '.assets | length')
48+
if [[ "$ASSET_COUNT" -lt 5 ]]; then
49+
echo "::error::Release $TAG has only $ASSET_COUNT assets, expected at least 5"
50+
exit 1
51+
fi
52+
53+
# Verify Publish Crate workflow also succeeded for this tag
54+
CRATE_STATUS=$(gh run list --workflow="Publish Crate" --branch="$TAG" --limit=1 --json conclusion --jq '.[0].conclusion // "not_found"')
55+
if [[ "$CRATE_STATUS" != "success" ]]; then
56+
echo "::warning::Publish Crate status: $CRATE_STATUS — proceeding anyway (npm is independent)"
57+
fi
58+
59+
- name: Skip beta releases
60+
run: |
61+
TAG="${{ steps.tag.outputs.tag }}"
62+
if [[ "$TAG" == *beta* ]]; then
63+
echo "::notice::Skipping beta release $TAG"
64+
exit 1
65+
fi
66+
67+
- uses: actions/setup-node@v4
68+
with:
69+
node-version: 20
70+
registry-url: https://registry.npmjs.org
71+
72+
- name: Get version from tag
73+
id: version
74+
run: |
75+
TAG="${{ steps.tag.outputs.tag }}"
76+
VERSION="${TAG#v}"
77+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
78+
79+
- name: Download release binaries
80+
env:
81+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82+
run: |
83+
TAG="${{ steps.tag.outputs.tag }}"
84+
mkdir -p binaries
85+
gh release download "$TAG" --dir binaries/
86+
87+
- name: Publish platform packages
88+
env:
89+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
90+
run: |
91+
VERSION="${{ steps.version.outputs.version }}"
92+
93+
declare -A TARGETS=(
94+
["x86_64-unknown-linux-gnu"]="linux-x64"
95+
["aarch64-unknown-linux-gnu"]="linux-arm64"
96+
["x86_64-apple-darwin"]="darwin-x64"
97+
["aarch64-apple-darwin"]="darwin-arm64"
98+
["x86_64-pc-windows-msvc"]="win32-x64"
99+
)
100+
101+
for target in "${!TARGETS[@]}"; do
102+
platform="${TARGETS[$target]}"
103+
pkg_dir="npm/$platform"
104+
105+
# Set version
106+
cd "$pkg_dir"
107+
npm version "$VERSION" --no-git-tag-version --allow-same-version
108+
109+
# Copy binary
110+
mkdir -p bin
111+
if [[ "$platform" == "win32-x64" ]]; then
112+
cp "../../binaries/roam-${target}.exe" bin/roam.exe
113+
else
114+
cp "../../binaries/roam-${target}" bin/roam
115+
chmod +x bin/roam
116+
fi
117+
118+
# Publish (skip if already published)
119+
npm publish --access public || echo "::warning::${platform} already published, skipping"
120+
cd ../..
121+
done
122+
123+
- name: Publish main package
124+
env:
125+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
126+
run: |
127+
VERSION="${{ steps.version.outputs.version }}"
128+
cd npm/roam-tui
129+
130+
# Update version
131+
npm version "$VERSION" --no-git-tag-version --allow-same-version
132+
133+
# Update optionalDependencies versions to match
134+
node -e "
135+
const fs = require('fs');
136+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
137+
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
138+
pkg.optionalDependencies[dep] = '$VERSION';
139+
}
140+
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
141+
"
142+
143+
# Publish (skip if already published)
144+
npm publish --access public || echo "::warning::Main package already published, skipping"

0 commit comments

Comments
 (0)