feat: add frictionless RA-H OS setup

- Add MCP setup, doctor, init-db, config, and rules CLI commands
- Publish latest MCP package wiring and local setup helper
- Update OS install docs for MCP-only, full app, and demo-safe setup

Generated with Claude Code
This commit is contained in:
“BeeRad”
2026-04-21 08:29:31 +10:00
parent 14b5784cd6
commit 21772fcba7
17 changed files with 718 additions and 49 deletions
+36 -12
View File
@@ -1,44 +1,54 @@
# RA-H MCP Server
Connect Claude Code and Claude Desktop to your RA-H knowledge base. This package talks directly to an existing RA-H SQLite database.
Connect Claude Code, Claude Desktop, Cursor, Codex, and other MCP clients to your RA-H knowledge base. This package talks directly to the local RA-H SQLite database.
## Install
## Quick Install
```bash
npx --yes ra-h-mcp-server@2.1.2
npx -y ra-h-mcp-server@latest setup --client claude-code --yes
```
Run RA-H once first so the database exists, then use the MCP server from any client.
The setup command creates or verifies the database, prints or writes MCP config for the selected client, and runs `doctor`.
Other useful commands:
```bash
npx -y ra-h-mcp-server@latest setup --client cursor --yes
npx -y ra-h-mcp-server@latest setup --client codex
npx -y ra-h-mcp-server@latest init-db
npx -y ra-h-mcp-server@latest doctor
npx -y ra-h-mcp-server@latest print-config --client claude-code
```
`--yes` lets the installer write supported JSON client config automatically. Codex uses TOML config, so the installer prints the block to add.
Important contract:
- pinned package version recommended
- `@latest` is the default user-facing install path
- exact versions are only for release/debug reproducibility
- standalone MCP reads and writes node data directly
- standalone MCP does not own chunking, embeddings, or live schema migration on an existing DB
## Configure Claude Code / Claude Desktop
Add to your Claude config (`~/.claude.json` or Claude Desktop settings):
Prefer the setup command above. Manual config is still available for troubleshooting:
```json
{
"mcpServers": {
"ra-h": {
"command": "npx",
"args": ["--yes", "ra-h-mcp-server@2.1.2"]
"args": ["-y", "ra-h-mcp-server@latest"]
}
}
}
```
Restart Claude fully. Done.
If you publish a newer MCP release and need this client to pick it up immediately, bump the pinned version here and restart Claude. Do not assume plain `npx ra-h-mcp-server` always refreshes instantly.
Restart Claude fully. If you need to freeze behavior for debugging, pin an exact version intentionally and restart the client.
## Requirements
- Node.js 18-22 LTS recommended
- an existing RA-H database at `~/Library/Application Support/RA-H/db/rah.sqlite`
- run the RA-H app at least once before using the standalone MCP server
- a RA-H database at `~/Library/Application Support/RA-H/db/rah.sqlite`, created by `setup`, `init-db`, or the app
## Environment Variables
@@ -46,6 +56,15 @@ If you publish a newer MCP release and need this client to pick it up immediatel
|----------|---------|-------------|
| `RAH_DB_PATH` | `~/Library/Application Support/RA-H/db/rah.sqlite` | Database path |
For demos or isolated installs:
```bash
npx -y ra-h-mcp-server@latest setup \
--client claude-code \
--yes \
--db "$HOME/Desktop/ra-h_os-demo-data/rah.sqlite"
```
## What To Expect
Once connected, the agent should:
@@ -98,6 +117,7 @@ Do not create contradictory instruction files. Prefer one short reinforcement li
Verification tip:
- after config changes, fully restart the client and confirm you can see tools like `queryNodes`, `retrieveQueryContext`, and `createNode`
- run `npx -y ra-h-mcp-server@latest doctor` to verify package version, DB path, schema health, and node count
## Node Metadata Contract
@@ -152,6 +172,10 @@ This is a lightweight CRUD server. Advanced features are handled by the main app
# Test database connection
node -e "const {initDatabase,query}=require('./services/sqlite-client');initDatabase();console.log(query('SELECT COUNT(*) as c FROM nodes')[0].c,'nodes')"
# Test CLI setup path
node index.js init-db --db /tmp/rah-test.sqlite
node index.js doctor --db /tmp/rah-test.sqlite
# Run the server
node index.js
```
+446
View File
@@ -0,0 +1,446 @@
#!/usr/bin/env node
'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const Database = require('better-sqlite3');
const packageJson = require('./package.json');
const { getDatabasePath } = require('./services/sqlite-client');
const SUPPORTED_CLIENTS = new Set([
'claude-code',
'claude-desktop',
'cursor',
'codex',
'opencode',
'vscode',
'windsurf',
'aider',
]);
function log(message) {
console.log(`[ra-h-mcp-server] ${message}`);
}
function fail(message) {
console.error(`[ra-h-mcp-server] ERROR: ${message}`);
process.exit(1);
}
function usage() {
console.log(`RA-H MCP Server ${packageJson.version}
Usage:
ra-h-mcp-server Start MCP stdio server
ra-h-mcp-server setup --client <name> Configure MCP for an agent
ra-h-mcp-server doctor Verify package, DB, and schema
ra-h-mcp-server init-db Create/verify the RA-H SQLite DB
ra-h-mcp-server print-config --client <name>
ra-h-mcp-server install-rules --client <name> [--target <path>]
Options:
--client <name> claude-code, claude-desktop, cursor, codex, opencode, vscode, windsurf, aider
--db <path> Override DB path for this command
--scope <scope> user or project (default: user)
--pin current Use this package version instead of @latest in generated config
--yes Write supported config files without prompting
--print-only Print config/rules without writing files
--target <path> Directory for project rule files
`);
}
function parseArgs(argv) {
const args = {
_: [],
client: null,
db: null,
scope: 'user',
pin: null,
yes: false,
printOnly: false,
target: null,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--help' || arg === '-h') {
args.help = true;
} else if (arg === '--yes' || arg === '-y') {
args.yes = true;
} else if (arg === '--print-only' || arg === '--dry-run') {
args.printOnly = true;
} else if (['--client', '--db', '--scope', '--pin', '--target'].includes(arg)) {
const value = argv[i + 1];
if (!value || value.startsWith('--')) fail(`${arg} requires a value`);
args[arg.slice(2)] = value;
i += 1;
} else {
args._.push(arg);
}
}
return args;
}
function expandPath(rawPath) {
if (!rawPath) return rawPath;
let expanded = rawPath;
if (expanded.startsWith('~')) {
expanded = path.join(os.homedir(), expanded.slice(1));
}
expanded = expanded.replace(/\$HOME/g, os.homedir());
return path.resolve(expanded);
}
function defaultDbPath() {
return getDatabasePath();
}
function resolveDbPath(args) {
return expandPath(args.db || process.env.RAH_DB_PATH || process.env.SQLITE_DB_PATH || defaultDbPath());
}
function ensureMinimumSchema(db) {
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
source TEXT,
link TEXT,
event_date TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
metadata TEXT,
embedding BLOB,
embedding_updated_at TEXT,
embedding_text TEXT,
chunk_status TEXT DEFAULT 'not_chunked'
);
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY,
node_id INTEGER NOT NULL,
chunk_idx INTEGER,
text TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
embedding_type TEXT DEFAULT 'text-embedding-3-small',
metadata TEXT,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_chunks_by_node ON chunks(node_id);
CREATE INDEX IF NOT EXISTS idx_chunks_by_node_idx ON chunks(node_id, chunk_idx);
CREATE TABLE IF NOT EXISTS edges (
id INTEGER PRIMARY KEY,
from_node_id INTEGER NOT NULL,
to_node_id INTEGER NOT NULL,
source TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
context TEXT,
explanation TEXT,
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node_id);
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node_id);
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
title,
source,
description,
content='nodes',
content_rowid='id'
);
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
text,
content='chunks',
content_rowid='id'
);
`);
}
function initDb(dbPath) {
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
const db = new Database(dbPath);
try {
ensureMinimumSchema(db);
const quickCheck = db.prepare('PRAGMA quick_check').get();
return quickCheck.quick_check || Object.values(quickCheck)[0];
} finally {
db.close();
}
}
function inspectDb(dbPath) {
if (!fs.existsSync(dbPath)) {
return { exists: false, ok: false, missing: ['database file'] };
}
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
try {
const requiredTables = ['nodes', 'edges', 'chunks'];
const missing = requiredTables.filter(
(table) => !db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(table)
);
const quickCheck = db.prepare('PRAGMA quick_check').get();
const nodeCount = missing.includes('nodes')
? null
: db.prepare('SELECT COUNT(*) as count FROM nodes').get().count;
return {
exists: true,
ok: missing.length === 0,
missing,
quickCheck: quickCheck.quick_check || Object.values(quickCheck)[0],
nodeCount,
};
} finally {
db.close();
}
}
function packageSpec(args) {
return args.pin === 'current'
? `ra-h-mcp-server@${packageJson.version}`
: 'ra-h-mcp-server@latest';
}
function mcpServerJson(args, dbPath) {
return {
command: 'npx',
args: ['-y', packageSpec(args)],
env: {
RAH_DB_PATH: dbPath,
},
};
}
function clientConfig(client, args, dbPath) {
const server = mcpServerJson(args, dbPath);
if (client === 'claude-code') {
return {
type: 'json-merge',
path: path.join(os.homedir(), '.claude.json'),
snippet: { mcpServers: { 'ra-h': server } },
writable: true,
};
}
if (client === 'claude-desktop') {
return {
type: 'json-merge',
path: path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
snippet: { mcpServers: { 'ra-h': server } },
writable: process.platform === 'darwin',
};
}
if (client === 'cursor') {
return {
type: 'json-merge',
path: path.join(os.homedir(), '.cursor', 'mcp.json'),
snippet: { mcpServers: { 'ra-h': server } },
writable: true,
};
}
if (client === 'vscode') {
return {
type: 'json-merge',
path: path.join(process.cwd(), '.vscode', 'mcp.json'),
snippet: { servers: { 'ra-h': server } },
writable: args.scope === 'project',
note: 'VS Code MCP config is project-local here; pass --scope project --yes to write it.',
};
}
if (client === 'codex') {
return {
type: 'toml',
path: path.join(os.homedir(), '.codex', 'config.toml'),
snippet: `[mcp_servers.ra-h]\ncommand = "npx"\nargs = ["-y", "${packageSpec(args)}"]\n\n[mcp_servers.ra-h.env]\nRAH_DB_PATH = "${dbPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\n`,
writable: false,
note: 'Codex config is TOML; this installer prints the block instead of mutating existing config.',
};
}
return {
type: 'json',
path: null,
snippet: { mcpServers: { 'ra-h': server } },
writable: false,
note: `${client} config writing is not automated yet. Copy this MCP block into that client's MCP settings.`,
};
}
function readJson(filePath) {
if (!fs.existsSync(filePath)) return {};
const raw = fs.readFileSync(filePath, 'utf8').trim();
if (!raw) return {};
return JSON.parse(raw);
}
function writeJsonMerge(config) {
const current = readJson(config.path);
const next = {
...current,
mcpServers: {
...(current.mcpServers || {}),
...(config.snippet.mcpServers || {}),
},
};
if (config.snippet.servers) {
next.servers = {
...(current.servers || {}),
...config.snippet.servers,
};
}
fs.mkdirSync(path.dirname(config.path), { recursive: true });
fs.writeFileSync(config.path, `${JSON.stringify(next, null, 2)}\n`);
}
function formatSnippet(snippet) {
return typeof snippet === 'string' ? snippet : JSON.stringify(snippet, null, 2);
}
function validateClient(client) {
if (!client) fail('Missing --client');
if (!SUPPORTED_CLIENTS.has(client)) {
fail(`Unsupported client "${client}". Supported: ${Array.from(SUPPORTED_CLIENTS).join(', ')}`);
}
}
function rulesSnippet() {
return `You are helping build a thoughtful graph of atomic units of context.
- Before substantive work that touches the user's projects, ideas, people, decisions, or prior context, retrieve relevant graph context from RA-H.
- Use queryNodes for direct lookup of a specific existing node.
- Use retrieveQueryContext for broader grounding before answering.
- Search before creating. Prefer updating the same artifact when it is clearly the same thing.
- When the user states a durable decision, preference, project fact, or useful idea, surface one concise candidate node for confirmation.
- Do not pester. Ask at most once per turn, and drop it if the user moves on.
- Preserve the user's wording in source for user-authored ideas unless they explicitly want a rewrite.
`;
}
function rulesTarget(client, target) {
const base = expandPath(target || process.cwd());
if (client === 'cursor') return path.join(base, '.cursor', 'rules', 'ra-h.mdc');
if (client === 'aider') return path.join(base, 'CONVENTIONS.md');
if (client === 'claude-code' || client === 'claude-desktop') return path.join(base, 'CLAUDE.md');
return path.join(base, 'AGENTS.md');
}
function commandInitDb(args) {
const dbPath = resolveDbPath(args);
const quickCheck = initDb(dbPath);
log(`Database ready at ${dbPath}`);
log(`SQLite quick_check: ${quickCheck}`);
}
function commandDoctor(args) {
const dbPath = resolveDbPath(args);
const report = inspectDb(dbPath);
let latest = null;
const npmResult = spawnSync('npm', ['view', 'ra-h-mcp-server', 'version'], {
encoding: 'utf8',
timeout: 5000,
});
if (npmResult.status === 0) latest = npmResult.stdout.trim();
log(`Package version: ${packageJson.version}`);
if (latest) log(`npm latest: ${latest}`);
log(`DB path: ${dbPath}`);
if (!report.exists) fail(`Database does not exist at ${dbPath}. Run init-db or setup first.`);
if (!report.ok) fail(`Database schema missing: ${report.missing.join(', ')}`);
log(`SQLite quick_check: ${report.quickCheck}`);
log(`Node count: ${report.nodeCount}`);
log('Doctor passed.');
}
function commandPrintConfig(args) {
validateClient(args.client);
const dbPath = resolveDbPath(args);
const config = clientConfig(args.client, args, dbPath);
console.log(formatSnippet(config.snippet));
if (config.note) log(config.note);
if (config.path) log(`Target path: ${config.path}`);
}
function commandInstallRules(args) {
validateClient(args.client);
const content = rulesSnippet();
const targetPath = rulesTarget(args.client, args.target);
if (args.printOnly || !args.yes) {
console.log(content);
log(`Rule file target: ${targetPath}`);
log('Pass --yes to write it.');
return;
}
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
log(`Wrote rules to ${targetPath}`);
}
function commandSetup(args) {
validateClient(args.client);
const dbPath = resolveDbPath(args);
initDb(dbPath);
log(`Database ready at ${dbPath}`);
const config = clientConfig(args.client, args, dbPath);
console.log(formatSnippet(config.snippet));
if (config.path) log(`MCP config target: ${config.path}`);
if (config.note) log(config.note);
const canWrite = config.writable && config.type === 'json-merge' && config.path;
if (canWrite && args.yes && !args.printOnly) {
writeJsonMerge(config);
log(`Updated ${config.path}`);
} else if (canWrite) {
log('Pass --yes to write this config automatically.');
} else {
log('Automatic config writing is not available for this client; copy the printed config.');
}
const rulePath = rulesTarget(args.client, args.target);
log(`Recommended rules target: ${rulePath}`);
log(`Install rules with: npx -y ${packageSpec(args)} install-rules --client ${args.client} --target <repo> --yes`);
commandDoctor(args);
}
function runCli(argv) {
const [command, ...rest] = argv;
const args = parseArgs(rest);
if (!command || command === '--help' || command === '-h' || args.help) {
usage();
return;
}
if (command === 'setup') return commandSetup(args);
if (command === 'doctor') return commandDoctor(args);
if (command === 'init-db') return commandInitDb(args);
if (command === 'print-config') return commandPrintConfig(args);
if (command === 'install-rules') return commandInstallRules(args);
fail(`Unknown command "${command}"`);
}
module.exports = {
runCli,
initDb,
inspectDb,
rulesSnippet,
};
+7
View File
@@ -36,6 +36,13 @@ const skillService = require('./services/skillService');
const retrievalService = require('./services/retrievalService');
const { directNodeLookup } = require('./services/directNodeLookupService');
const cliCommands = new Set(['setup', 'doctor', 'init-db', 'print-config', 'install-rules', '--help', '-h']);
if (cliCommands.has(process.argv[2])) {
const { runCli } = require('./cli');
runCli(process.argv.slice(2));
process.exit(0);
}
// Server info
const serverInfo = {
name: 'ra-h-standalone',
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "ra-h-mcp-server",
"version": "2.1.2",
"version": "3.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ra-h-mcp-server",
"version": "2.1.2",
"version": "3.2.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
@@ -17,7 +17,7 @@
"ra-h-mcp-server": "index.js"
},
"engines": {
"node": ">=18.0.0"
"node": ">=18.0.0 <24.0.0"
}
},
"node_modules/@hono/node-server": {
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "ra-h-mcp-server",
"version": "2.1.2",
"version": "3.2.0",
"description": "Connect Claude Code/Desktop to your RA-H knowledge base. Direct SQLite access to an existing RA-H database.",
"main": "index.js",
"bin": {
@@ -8,7 +8,7 @@
},
"scripts": {
"start": "node index.js",
"validate:syntax": "node --check index.js && node --check services/*.js",
"validate:syntax": "node --check index.js && node --check cli.js && node --check services/*.js",
"validate:publish": "npm run validate:syntax && npm pack --dry-run >/dev/null",
"prepublishOnly": "node ../../scripts/dev/sync-skills.mjs --check && npm run validate:publish"
},
@@ -29,11 +29,11 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bradwmorris/ra-h.git"
"url": "git+https://github.com/bradwmorris/ra-h_os.git"
},
"homepage": "https://ra-h.app",
"bugs": {
"url": "https://github.com/bradwmorris/ra-h/issues"
"url": "https://github.com/bradwmorris/ra-h_os/issues"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
@@ -45,6 +45,7 @@
},
"files": [
"index.js",
"cli.js",
"services/",
"skills/",
"README.md"