Compare commits

...
Author SHA1 Message Date
root 714e21db42 feat: auto-register gbrain MCP server in OpenClaw config during setup
Adds `gbrain register-mcp` CLI command and integrates MCP registration
into `gbrain init`. When a user sets up gbrain, it now automatically
adds `mcp.servers.gbrain` to their OpenClaw config so all 39 gbrain
tools are natively discoverable by every agent session and sub-agent.

- `src/commands/register-mcp.ts` — standalone registration function
  - Reads OpenClaw config (OPENCLAW_CONFIG_PATH or ~/.openclaw/openclaw.json)
  - Adds mcp.servers.gbrain with command/args/env from gbrain config
  - Atomic write (tmp + rename), idempotent (skips if already registered)
  - Graceful skip if OpenClaw config doesn't exist yet
- `src/cli.ts` — `gbrain register-mcp` command registration
- `src/commands/init.ts` — calls registerMcpServerInOpenClaw at end of setup
- `test/register-mcp.test.ts` — 6 tests (empty config, alongside existing,
  idempotent, missing config, preserves fields, null config fallback)

After running `gbrain init` or `gbrain register-mcp`, users restart
their OpenClaw gateway and gbrain tools appear natively — no shell exec
wrappers, no manual config editing.
2026-04-24 05:21:03 +00:00
4 changed files with 308 additions and 1 deletions
+7 -1
View File
@@ -19,7 +19,7 @@ for (const op of operations) {
}
// CLI-only commands that bypass the operation layer
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable', 'routing-eval', 'skillify']);
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'register-mcp']);
async function main() {
// Parse global flags (--quiet / --progress-json / --progress-interval)
@@ -381,6 +381,12 @@ async function handleCliOnly(command: string, args: string[]) {
return;
}
if (command === 'register-mcp') {
const { runRegisterMcp } = await import('./commands/register-mcp.ts');
await runRegisterMcp(args);
return;
}
if (command === 'dream') {
// Dream mirrors doctor's pattern: filesystem phases run without a DB,
// so an engine connection failure is non-fatal. runCycle honestly
+27
View File
@@ -8,6 +8,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { saveConfig, loadConfig, toEngineConfig, type GBrainConfig } from '../core/config.ts';
import { createEngine } from '../core/engine-factory.ts';
import { registerMcpServerInOpenClaw } from './register-mcp.ts';
export async function runInit(args: string[]) {
const isSupabase = args.includes('--supabase');
@@ -117,6 +118,7 @@ async function initPGLite(opts: { jsonOutput: boolean; apiKey: string | null; cu
...(opts.apiKey ? { openai_api_key: opts.apiKey } : {}),
};
saveConfig(config);
const mcpRegistration = registerMcpServerInOpenClaw({ gbrainConfig: config });
const stats = await engine.getStats();
@@ -134,6 +136,18 @@ async function initPGLite(opts: { jsonOutput: boolean; apiKey: string | null; cu
} else {
console.log('Next: gbrain import <dir>');
}
if (mcpRegistration.status === 'registered') {
console.log(`OpenClaw MCP: registered gbrain at ${mcpRegistration.openclawConfigPath}`);
console.log('Restart your OpenClaw gateway so all gbrain tools are discoverable.');
} else if (mcpRegistration.status === 'already_registered') {
console.log('OpenClaw MCP: gbrain already registered.');
console.log('Restart your OpenClaw gateway if it is currently running.');
} else if (mcpRegistration.status === 'missing_openclaw_config') {
console.log(`OpenClaw MCP: skipped (config not found at ${mcpRegistration.openclawConfigPath}).`);
console.log('Run `gbrain register-mcp` after OpenClaw creates its config.');
} else {
console.log('OpenClaw MCP: skipped (no gbrain config available).');
}
console.log('');
console.log('When you outgrow local: gbrain migrate --to supabase');
reportModStatus();
@@ -200,6 +214,7 @@ async function initPostgres(opts: { databaseUrl: string; jsonOutput: boolean; ap
...(opts.apiKey ? { openai_api_key: opts.apiKey } : {}),
};
saveConfig(config);
const mcpRegistration = registerMcpServerInOpenClaw({ gbrainConfig: config });
console.log('Config saved to ~/.gbrain/config.json');
const stats = await engine.getStats();
@@ -217,6 +232,18 @@ async function initPostgres(opts: { databaseUrl: string; jsonOutput: boolean; ap
} else {
console.log('Next: gbrain import <dir>');
}
if (mcpRegistration.status === 'registered') {
console.log(`OpenClaw MCP: registered gbrain at ${mcpRegistration.openclawConfigPath}`);
console.log('Restart your OpenClaw gateway so all gbrain tools are discoverable.');
} else if (mcpRegistration.status === 'already_registered') {
console.log('OpenClaw MCP: gbrain already registered.');
console.log('Restart your OpenClaw gateway if it is currently running.');
} else if (mcpRegistration.status === 'missing_openclaw_config') {
console.log(`OpenClaw MCP: skipped (config not found at ${mcpRegistration.openclawConfigPath}).`);
console.log('Run `gbrain register-mcp` after OpenClaw creates its config.');
} else {
console.log('OpenClaw MCP: skipped (no gbrain config available).');
}
reportModStatus();
}
} finally {
+142
View File
@@ -0,0 +1,142 @@
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { homedir } from 'os';
import type { GBrainConfig } from '../core/config.ts';
import { loadConfig } from '../core/config.ts';
type JsonObject = Record<string, unknown>;
export type RegisterMcpStatus =
| 'registered'
| 'already_registered'
| 'missing_openclaw_config'
| 'missing_gbrain_config';
export interface RegisterMcpResult {
status: RegisterMcpStatus;
openclawConfigPath: string;
}
interface RegisterMcpOptions {
openclawConfigPath?: string;
gbrainInstallPath?: string;
gbrainConfig?: GBrainConfig | null;
}
function resolveOpenClawConfigPath(): string {
return process.env.OPENCLAW_CONFIG_PATH
|| join(homedir(), '.openclaw', 'openclaw.json');
}
function getInstallPath(): string {
return dirname(dirname(__dirname));
}
function readJsonObject(path: string): JsonObject {
const raw = readFileSync(path, 'utf-8');
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`OpenClaw config must be a JSON object: ${path}`);
}
return parsed as JsonObject;
}
function toObject(value: unknown): JsonObject {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return value as JsonObject;
}
function atomicWriteJson(path: string, data: JsonObject): void {
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
try {
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
renameSync(tmpPath, path);
} catch (err) {
try { unlinkSync(tmpPath); } catch { /* best-effort */ }
throw err;
}
}
function buildGbrainServer(installPath: string, config: GBrainConfig): JsonObject {
const env: JsonObject = {};
if (config.database_url) {
env.GBRAIN_DATABASE_URL = config.database_url;
}
return {
command: 'bun',
args: ['run', join(installPath, 'src', 'cli.ts'), 'serve'],
env,
};
}
export function registerMcpServerInOpenClaw(opts: RegisterMcpOptions = {}): RegisterMcpResult {
const openclawConfigPath = opts.openclawConfigPath || resolveOpenClawConfigPath();
if (!existsSync(openclawConfigPath)) {
return { status: 'missing_openclaw_config', openclawConfigPath };
}
const gbrainConfig = opts.gbrainConfig ?? loadConfig();
if (!gbrainConfig) {
return { status: 'missing_gbrain_config', openclawConfigPath };
}
const config = readJsonObject(openclawConfigPath);
const mcp = toObject(config.mcp);
const servers = toObject(mcp.servers);
if (servers.gbrain !== undefined) {
return { status: 'already_registered', openclawConfigPath };
}
const installPath = opts.gbrainInstallPath || getInstallPath();
const next: JsonObject = {
...config,
mcp: {
...mcp,
servers: {
...servers,
gbrain: buildGbrainServer(installPath, gbrainConfig),
},
},
};
atomicWriteJson(openclawConfigPath, next);
return { status: 'registered', openclawConfigPath };
}
function printHelp() {
console.log(`Usage: gbrain register-mcp
Add gbrain's MCP stdio server to your OpenClaw config (idempotent).
OpenClaw config path:
$OPENCLAW_CONFIG_PATH
~/.openclaw/openclaw.json (default)
`);
}
export async function runRegisterMcp(args: string[]): Promise<void> {
if (args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
const result = registerMcpServerInOpenClaw();
if (result.status === 'registered') {
console.log(`Registered gbrain MCP server in ${result.openclawConfigPath}.`);
console.log('Restart your OpenClaw gateway so gbrain tools are discoverable.');
return;
}
if (result.status === 'already_registered') {
console.log(`OpenClaw config already has mcp.servers.gbrain (${result.openclawConfigPath}).`);
console.log('Restart your OpenClaw gateway if it is already running.');
return;
}
if (result.status === 'missing_openclaw_config') {
console.log(`OpenClaw config not found at ${result.openclawConfigPath}. Skipping MCP registration.`);
console.log('Create/open OpenClaw once, then run: gbrain register-mcp');
return;
}
console.log('No gbrain config found. Run `gbrain init` first, then `gbrain register-mcp`.');
}
+132
View File
@@ -0,0 +1,132 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { registerMcpServerInOpenClaw } from '../src/commands/register-mcp.ts';
function tmpDir() {
return mkdtempSync(join(tmpdir(), 'register-mcp-test-'));
}
function writeJson(path: string, data: object) {
writeFileSync(path, JSON.stringify(data, null, 2));
}
function readJson(path: string) {
return JSON.parse(readFileSync(path, 'utf-8'));
}
describe('registerMcpServerInOpenClaw', () => {
let dir: string;
beforeEach(() => {
dir = tmpDir();
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
test('adds gbrain MCP server to empty config', () => {
const configPath = join(dir, 'openclaw.json');
writeJson(configPath, {});
const result = registerMcpServerInOpenClaw({
openclawConfigPath: configPath,
gbrainInstallPath: '/opt/gbrain',
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
});
expect(result.status).toBe('registered');
const config = readJson(configPath);
expect(config.mcp.servers.gbrain).toBeDefined();
expect(config.mcp.servers.gbrain.command).toBe('bun');
expect(config.mcp.servers.gbrain.args).toContain('serve');
expect(config.mcp.servers.gbrain.env.GBRAIN_DATABASE_URL).toBe('postgres://localhost/brain');
});
test('adds alongside existing MCP servers', () => {
const configPath = join(dir, 'openclaw.json');
writeJson(configPath, {
mcp: {
servers: {
circleback: { url: 'https://app.circleback.ai/api/mcp' },
},
},
});
const result = registerMcpServerInOpenClaw({
openclawConfigPath: configPath,
gbrainInstallPath: '/opt/gbrain',
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
});
expect(result.status).toBe('registered');
const config = readJson(configPath);
expect(config.mcp.servers.circleback).toBeDefined();
expect(config.mcp.servers.gbrain).toBeDefined();
});
test('skips if already registered (idempotent)', () => {
const configPath = join(dir, 'openclaw.json');
writeJson(configPath, {
mcp: { servers: { gbrain: { command: 'bun', args: ['serve'] } } },
});
const result = registerMcpServerInOpenClaw({
openclawConfigPath: configPath,
gbrainInstallPath: '/opt/gbrain',
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
});
expect(result.status).toBe('already_registered');
});
test('handles missing OpenClaw config gracefully', () => {
const result = registerMcpServerInOpenClaw({
openclawConfigPath: join(dir, 'nonexistent.json'),
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
});
expect(result.status).toBe('missing_openclaw_config');
});
test('preserves all existing config fields', () => {
const configPath = join(dir, 'openclaw.json');
const original = {
agents: { defaults: { model: 'claude-3' } },
gateway: { port: 18789 },
plugins: { entries: { telegram: { enabled: true } } },
mcp: { servers: { other: { url: 'https://example.com' } } },
};
writeJson(configPath, original);
registerMcpServerInOpenClaw({
openclawConfigPath: configPath,
gbrainInstallPath: '/opt/gbrain',
gbrainConfig: { database_url: 'postgres://localhost/brain', engine: 'postgres' },
});
const config = readJson(configPath);
expect(config.agents.defaults.model).toBe('claude-3');
expect(config.gateway.port).toBe(18789);
expect(config.plugins.entries.telegram.enabled).toBe(true);
expect(config.mcp.servers.other.url).toBe('https://example.com');
expect(config.mcp.servers.gbrain).toBeDefined();
});
test('handles null gbrainConfig by falling back to loadConfig', () => {
const configPath = join(dir, 'openclaw.json');
writeJson(configPath, {});
// When gbrainConfig is null AND loadConfig returns null, status is missing_gbrain_config.
// But on dev machines loadConfig() may succeed, so we just verify it doesn't crash
// and returns a valid status.
const result = registerMcpServerInOpenClaw({
openclawConfigPath: configPath,
gbrainConfig: null,
});
expect(['missing_gbrain_config', 'registered']).toContain(result.status);
});
});