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
2 changes: 1 addition & 1 deletion .well-known/mcp/server-card.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"serverInfo": {
"name": "pilotprotocol-mcp",
"version": "0.2.12"
"version": "0.2.13"
},
"authentication": {
"scheme": "local-daemon",
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ All notable changes to the `pilotprotocol-mcp` npm adapter are documented here.

## [Unreleased]

## [0.2.13] - 2026-08-07

### Fixed
- `attach --all` skips an unavailable optional OpenClaw host without abandoning every other harness.
- PicoClaw is only reported as attached when its host configuration exists; explicit PicoClaw attachment now fails precisely instead of silently doing nothing.

## [0.2.12] - 2026-08-07

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pilotprotocol-mcp",
"version": "0.2.12",
"version": "0.2.13",
"mcpName": "io.github.pilot-protocol/pilot-mcp",
"description": "Your agent's overlay network. MCP server exposing 436 Pilot specialist agents + P2P A2A messaging. One install configures every harness on your machine.",
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"url": "https://github.com/pilot-protocol/pilot-mcp",
"source": "github"
},
"version": "0.2.12",
"version": "0.2.13",
"websiteUrl": "https://pilotprotocol.network",
"packages": [
{
"registryType": "npm",
"identifier": "pilotprotocol-mcp",
"version": "0.2.12",
"version": "0.2.13",
"transport": { "type": "stdio" }
}
]
Expand Down
2 changes: 1 addition & 1 deletion src/openclaw-plugin/evaluate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from 'node:child_process';

const PACKAGE_SPEC = 'pilotprotocol-mcp@0.2.12';
const PACKAGE_SPEC = 'pilotprotocol-mcp@0.2.13';
const DEFAULT_TIMEOUT_MS = 20_000;
const MAX_STDERR_BYTES = 1 << 20;

Expand Down
2 changes: 1 addition & 1 deletion src/openclaw-plugin/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"pilot": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "pilotprotocol-mcp@0.2.12"]
"args": ["-y", "pilotprotocol-mcp@0.2.13"]
}
}
}
19 changes: 17 additions & 2 deletions src/setup/attach.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,32 @@ export async function runAttach(flags, options = {}) {
const selected = selectedHarnesses(flags);
const writers = options.harnesses ?? harnesses;
const configured = [];
const skipped = [];
for (const id of selected) {
const writer = writers[id];
if (!writer?.configure) throw new Error(`unsupported harness ${id}`);
await writer.configure({ id, name: id, transport: 'managed', enterpriseControl: controlPath });
const result = await writer.configure({
id,
name: id,
transport: 'managed',
enterpriseControl: controlPath,
home,
allowMissingHost: flags.all === true,
});
if (result?.skipped === true) {
skipped.push({ id, reason: result.reason || 'host is unavailable' });
continue;
}
configured.push(id);
}

const write = options.write ?? ((message) => process.stdout.write(`${message}\n`));
write(`Attached ${configured.join(', ')} to the existing core Pilot node.`);
if (skipped.length > 0) {
write(`Skipped ${skipped.map(({ id, reason }) => `${id} (${reason})`).join(', ')}.`);
}
write('Core runtime and node identity were not changed.');
return { controlPath, configured };
return { controlPath, configured, skipped };
}

export function selectedHarnesses(flags) {
Expand Down
49 changes: 29 additions & 20 deletions src/setup/harnesses/openclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,43 @@ import { homedir } from 'node:os';
import { fileURLToPath, URL } from 'node:url';
import { promisify } from 'node:util';

const HOME = homedir();
const CONFIG = join(HOME, '.openclaw', 'openclaw.json');
const SOURCE_PLUGIN = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..', 'openclaw-plugin');
const INSTALLED_PLUGIN = join(HOME, '.pilot', 'integrations', 'openclaw-policy');
const execFileAsync = promisify(execFile);

export async function configure() {
removeObsoleteMcpEntry();
mkdirSync(join(HOME, '.pilot', 'integrations'), { recursive: true });
cpSync(SOURCE_PLUGIN, INSTALLED_PLUGIN, { recursive: true, force: true });
await execFileAsync('openclaw', ['plugins', 'install', '--link', '--force', INSTALLED_PLUGIN], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
await execFileAsync('openclaw', ['plugins', 'enable', 'pilot-policy'], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
await execFileAsync('openclaw', ['plugins', 'inspect', 'pilot-policy', '--json'], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
export async function configure(options = {}) {
const home = options.home ?? homedir();
const config = join(home, '.openclaw', 'openclaw.json');
const installedPlugin = join(home, '.pilot', 'integrations', 'openclaw-policy');
const execute = options.execFileAsync ?? execFileAsync;
removeObsoleteMcpEntry(config);
mkdirSync(join(home, '.pilot', 'integrations'), { recursive: true });
cpSync(SOURCE_PLUGIN, installedPlugin, { recursive: true, force: true });
try {
await execute('openclaw', ['plugins', 'install', '--link', '--force', installedPlugin], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
await execute('openclaw', ['plugins', 'enable', 'pilot-policy'], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
await execute('openclaw', ['plugins', 'inspect', 'pilot-policy', '--json'], {
env: process.env, timeout: 60000, maxBuffer: 1 << 20,
});
} catch (error) {
if (options.allowMissingHost === true && error?.code === 'ENOENT') {
return { skipped: true, reason: 'OpenClaw CLI is not installed' };
}
throw error;
}
return { skipped: false };
}

function removeObsoleteMcpEntry() {
if (!existsSync(CONFIG)) return;
const current = JSON.parse(readFileSync(CONFIG, 'utf8'));
function removeObsoleteMcpEntry(config) {
if (!existsSync(config)) return;
const current = JSON.parse(readFileSync(config, 'utf8'));
if (isPilotMcp(current.mcpServers?.pilot)) {
delete current.mcpServers.pilot;
if (Object.keys(current.mcpServers).length === 0) delete current.mcpServers;
writeFileSync(CONFIG, JSON.stringify(current, null, 2));
writeFileSync(config, JSON.stringify(current, null, 2));
}
}

Expand Down
18 changes: 11 additions & 7 deletions src/setup/harnesses/picoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
import { PILOT_PACKAGE_SPEC, pilotMcpServer } from './runtime.js';

const HOME = homedir();
const CONFIG = join(HOME, '.picoclaw', 'config.json');

export async function configure() {
if (!existsSync(CONFIG)) return;
const current = JSON.parse(readFileSync(CONFIG, 'utf8'));
export async function configure(options = {}) {
const config = join(options.home ?? homedir(), '.picoclaw', 'config.json');
if (!existsSync(config)) {
if (options.allowMissingHost === true) {
return { skipped: true, reason: 'PicoClaw configuration was not found' };
}
throw new Error(`PicoClaw configuration was not found at ${config}`);
}
const current = JSON.parse(readFileSync(config, 'utf8'));
current.tools = current.tools ?? {};
current.tools.mcp = current.tools.mcp ?? {};
current.tools.mcp.enabled = true;
Expand All @@ -31,5 +34,6 @@ export async function configure() {
command: ['npx', '-y', PILOT_PACKAGE_SPEC, 'picoclaw-hook'],
intercept: ['before_tool', 'after_tool'],
};
writeFileSync(CONFIG, JSON.stringify(current, null, 2));
writeFileSync(config, JSON.stringify(current, null, 2));
return { skipped: false };
}
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// One source of truth for runtime and generated-configuration versioning.
// Release-contract tests keep this synchronized with package/registry metadata.
export const VERSION = '0.2.12';
export const VERSION = '0.2.13';
export const PACKAGE_SPEC = `pilotprotocol-mcp@${VERSION}`;
38 changes: 36 additions & 2 deletions test/attach.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { chmodSync, lstatSync, mkdirSync, mkdtempSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { runAttach, selectedHarnesses } from '../src/setup/attach.js';
import { ATTACHABLE_HARNESSES, runAttach, selectedHarnesses } from '../src/setup/attach.js';

test('adapter-only attach configures a harness without touching the core runtime', async () => {
const home = mkdtempSync(join(tmpdir(), 'pilot-attach-'));
Expand All @@ -24,11 +24,45 @@ test('adapter-only attach configures a harness without touching the core runtime
});

assert.deepEqual(result.configured, ['gemini']);
assert.deepEqual(result.skipped, []);
assert.equal(result.controlPath, control);
assert.deepEqual(calls, [{ id: 'gemini', name: 'gemini', transport: 'managed', enterpriseControl: control }]);
assert.deepEqual(calls, [{
id: 'gemini', name: 'gemini', transport: 'managed', enterpriseControl: control,
home, allowMissingHost: false,
}]);
assert.match(output.join('\n'), /Core runtime and node identity were not changed/);
});

test('attach --all skips a missing optional host without abandoning other harnesses', async () => {
const home = mkdtempSync(join(tmpdir(), 'pilot-attach-all-'));
const managed = join(home, '.pilot', 'managed');
mkdirSync(managed, { recursive: true, mode: 0o700 });
const control = join(managed, 'enterprise-control.json');
writeFileSync(control, '{"mode":"managed"}\n', { mode: 0o600 });
chmodSync(control, 0o600);
const calls = [];
const output = [];
const harnesses = Object.fromEntries(ATTACHABLE_HARNESSES.map((id) => [id, {
configure: async (options) => {
calls.push(options);
return id === 'openclaw' ? { skipped: true, reason: 'OpenClaw CLI is not installed' } : undefined;
},
}]));

const result = await runAttach({ all: true }, {
home,
lstat: lstatSync,
harnesses,
write: (line) => output.push(line),
});

assert.equal(calls.length, ATTACHABLE_HARNESSES.length);
assert.equal(calls.every((call) => call.allowMissingHost === true && call.home === home), true);
assert.deepEqual(result.configured, ATTACHABLE_HARNESSES.filter((id) => id !== 'openclaw'));
assert.deepEqual(result.skipped, [{ id: 'openclaw', reason: 'OpenClaw CLI is not installed' }]);
assert.match(output.join('\n'), /Skipped openclaw \(OpenClaw CLI is not installed\)/);
});

test('adapter-only attach requires an explicit harness and owner-only regular control', async () => {
assert.throws(() => selectedHarnesses({}), /choose at least one harness/);
await assert.rejects(
Expand Down
12 changes: 6 additions & 6 deletions test/harness-config-contracts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ test('Claude separates user MCP registration from hook settings and migrates sta
configureInHome('claude', home);

const mcp = JSON.parse(readFileSync(join(home, '.claude.json'), 'utf8'));
assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']);
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
assert.equal(settings.theme, 'dark');
assert.deepEqual(settings.mcpServers, { customer: { command: 'customer-mcp' } });
Expand All @@ -54,7 +54,7 @@ test('Gemini uses current MCP and BeforeTool/AfterTool user settings idempotentl
writeJSON(settingsPath, { mcpServers: { customer: { command: 'customer-mcp' } }, hooks: {} });
configureInHome('gemini', home);
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
assert.deepEqual(settings.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(settings.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']);
assert.equal(settings.mcpServers.customer.command, 'customer-mcp');
assert.equal(settings.hooksConfig.enabled, true);
assert.equal(settings.hooks.BeforeTool.length, 1);
Expand All @@ -75,7 +75,7 @@ test('Continue merges Pilot into config.yaml and removes only its obsolete dupli
const config = parse(source);
assert.match(source, /# customer config/);
assert.equal(config.mcpServers.filter((entry) => entry.name === 'Pilot').length, 1);
assert.deepEqual(config.mcpServers.find((entry) => entry.name === 'Pilot').args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(config.mcpServers.find((entry) => entry.name === 'Pilot').args, ['-y', 'pilotprotocol-mcp@0.2.13']);
assert.equal(config.mcpServers.find((entry) => entry.name === 'Customer').command, 'customer-mcp');
assert.equal(existsSync(legacyPath), false);
});
Expand All @@ -91,7 +91,7 @@ test('OpenHands migrates pre-1.0 TOML MCP config and installs project hooks', ()
configureInHome('openhands', home, { cwd: workspace });

const mcp = JSON.parse(readFileSync(join(home, '.openhands', 'mcp.json'), 'utf8'));
assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']);
assert.equal(mcp.mcpServers.customer.command, 'customer-mcp');
const legacy = readFileSync(legacyPath, 'utf8');
assert.doesNotMatch(legacy, /mcp\.stdio_servers\.pilot/);
Expand All @@ -110,7 +110,7 @@ test('Codex upgrades its owned TOML table without duplicating user configuration
configureInHome('codex', home);
const config = readFileSync(configPath, 'utf8');
assert.equal(config.match(/\[mcp_servers\.pilot\]/g)?.length, 1);
assert.match(config, /pilotprotocol-mcp@0\.2\.12/);
assert.match(config, /pilotprotocol-mcp@0\.2\.13/);
assert.match(config, /\[mcp_servers\.customer\]/);
assert.match(config, /model = "customer"/);
const hooks = JSON.parse(readFileSync(join(home, '.codex', 'hooks.json'), 'utf8'));
Expand All @@ -122,6 +122,6 @@ test('Junie writes the shared CLI and IDE user MCP location', () => {
const home = mkdtempSync(join(tmpdir(), 'pilot-junie-contract-'));
configureInHome('junie', home);
const config = JSON.parse(readFileSync(join(home, '.junie', 'mcp', 'mcp.json'), 'utf8'));
assert.deepEqual(config.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(config.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']);
assert.equal(existsSync(join(home, '.junie', 'config.json')), false);
});
8 changes: 4 additions & 4 deletions test/hermes-setup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ test('Hermes setup merges native pre/post hooks without replacing existing YAML'
assert.equal(result.model, 'gemini/example');
assert.equal(result.hooks.on_session_start[0].command, 'existing-hook');
assert.equal(result.hooks.pre_tool_call.length, 1);
assert.equal(result.hooks.pre_tool_call[0].command, 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase pre');
assert.equal(result.hooks.pre_tool_call[0].command, 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase pre');
assert.equal(result.hooks.post_tool_call.length, 1);
assert.deepEqual(result.mcp_servers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']);
assert.deepEqual(result.mcp_servers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']);
const allowlist = JSON.parse(readFileSync(join(home, '.hermes', 'shell-hooks-allowlist.json'), 'utf8'));
assert.deepEqual(allowlist.approvals, [
{ event: 'pre_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase pre' },
{ event: 'post_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase post' },
{ event: 'pre_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase pre' },
{ event: 'post_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase post' },
]);
});
Loading