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.1.0"
"version": "0.2.12"
},
"authentication": {
"scheme": "local-daemon",
Expand Down
25 changes: 22 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ All notable changes to the `pilotprotocol-mcp` npm adapter are documented here.

## [Unreleased]

## [0.2.12] - 2026-08-07

### Fixed
- Claude MCP registration now uses `~/.claude.json`, Cline uses its shared
`~/.cline` configuration, Copilot CLI uses `~/.copilot/mcp-config.json`,
OpenHands uses its post-1.0 `mcp.json`, and Junie uses
`~/.junie/mcp/mcp.json`.
- Continue, Codex, Hermes, OpenClaw, and PicoClaw setup now upgrades owned
entries idempotently, preserves unrelated configuration, enables the native
integration, and removes only obsolete Pilot-owned duplicates.
- Cline's current `tool` payload and its deployed `toolName` compatibility
shape are both normalized. Post-hook failures retain error, success, timing,
and result evidence for hosted tracing.
- Managed pre-hooks enforce a 20-second internal deadline and fail closed
before host-level timeout behavior can silently allow an action. OpenClaw's
native plugin applies the same deadline to tools and outbound messages.
- Version-pinned obsolete Claude heartbeat entries are migrated, while the
compatibility command remains a silent allow for already-running sessions.
- Runtime, registry, server-card, generated-hook, and OpenClaw plugin versions
are now release-contract tested against the npm package version.
- Generated hook and MCP commands now pin `pilotprotocol-mcp@0.2.12`.

## [0.2.11] - 2026-08-07

### Fixed
Expand Down Expand Up @@ -92,7 +114,4 @@ All notable changes to the `pilotprotocol-mcp` npm adapter are documented here.
### Pending (not yet implemented)
- Windows runtime bootstrap and per-platform npm binary subpackages.
- Streamable HTTP transport (`pilot-mcp serve --http`).
- `pilot-mcp doctor` diagnostic.
- `pilot-mcp tour` first-run demo.
- Privileged system-wide daemon service installation (the managed user runtime is installed and started automatically).
- Per-turn heartbeat hooks (Claude Code, OpenHands paths sketched).
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,25 @@ npx -y pilotprotocol-mcp setup
Or per-harness manual:

```bash
# Claude Code
# Claude Code — user MCP lives in ~/.claude.json; hooks live in ~/.claude/settings.json
claude mcp add --transport stdio pilot -- npx -y pilotprotocol-mcp

# Cursor — add to ~/.cursor/mcp.json
{"mcpServers":{"pilot":{"command":"npx","args":["-y", "pilotprotocol-mcp"]}}}

# Cline — add to cline_mcp_settings.json (same JSON)
# Cline — add the same JSON to ~/.cline/data/settings/cline_mcp_settings.json

# Continue.dev — add to .continue/mcpServers/pilot.yaml
name: Pilot
version: 0.0.1
# Continue.dev — merge into ~/.continue/config.yaml
name: My Continue Config
version: 1.0.0
schema: v1
mcpServers:
- name: pilot
- name: Pilot
command: npx
args: ["-y", "pilotprotocol-mcp"]

# OpenHands — add to ~/.openhands/config.toml
[mcp.stdio_servers.pilot]
command = "npx"
args = ["-y", "pilotprotocol-mcp"]
# OpenHands — add to ~/.openhands/mcp.json
{"mcpServers":{"pilot":{"command":"npx","args":["-y","pilotprotocol-mcp"]}}}

# Hermes — add to ~/.hermes/config.yaml
mcp_servers:
Expand All @@ -135,15 +133,27 @@ command = "npx"
args = ["-y", "pilotprotocol-mcp"]

# PicoClaw — add to ~/.picoclaw/config.json
{"tools":{"mcp":{"servers":{"pilot":{"command":"npx","args":["-y", "pilotprotocol-mcp"]}}}}}
{"tools":{"mcp":{"enabled":true,"servers":{"pilot":{"enabled":true,"command":"npx","args":["-y", "pilotprotocol-mcp"]}}}}}

# Copilot CLI — add standard MCP JSON to ~/.copilot/mcp-config.json

# Junie CLI/IDE — add standard MCP JSON to ~/.junie/mcp/mcp.json

# OpenClaw — setup installs and enables the Pilot Policy plugin
openclaw plugins inspect pilot-policy --runtime --json
```

## Privacy
## Privacy and optional management

- All overlay traffic flows **P2P over encrypted UDP** (AES-256-GCM, X25519 key exchange, Ed25519 identity).
- No telemetry. No SaaS in the middle. No API key registries.
- An unmanaged node does not upload tool calls and every installed policy hook
is a zero-side-effect pass-through.
- Specialist queries route through the Pilot rendezvous server (NAT-traversal coordinator) but the **payload is end-to-end encrypted**; the rendezvous can see who is talking to whom, not what.
- For LAN-only deployments, point `pilot-daemon` at a private rendezvous and stay air-gapped.
- When a node is explicitly adopted into Pilot Management, its pre/post action
envelopes—including tool inputs and results—are sent to the hosted federation
control plane for policy evaluation, approvals, and audit. That managed path
is opt-in and fail-closed for pre-action decisions.

## Comparison

Expand Down
4 changes: 1 addition & 3 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// pilot-mcp tour → first-run guided demo (one specialist call)
// pilot-mcp export-identity → write identity to portable file
// pilot-mcp import-identity <f> → load identity from portable file
// pilot-mcp uninstall → reverse setup (remove harness configs, optionally stop daemon)
// pilot-mcp <anything-else> → delegate to the platform pilotctl binary
//
// Critical: bare `pilot-mcp` invocation MUST start the stdio server immediately.
Expand Down Expand Up @@ -91,7 +90,7 @@ async function main() {
}
case 'export-identity':
case 'import-identity':
case 'uninstall': {
{
const { runLifecycle } = await import('./src/lifecycle.js');
await runLifecycle(cmd, args.slice(1));
break;
Expand Down Expand Up @@ -146,7 +145,6 @@ Usage:
pilot-mcp tour Guided first-run demo
pilot-mcp export-identity Write identity to portable file
pilot-mcp import-identity <file> Load identity from portable file
pilot-mcp uninstall Reverse setup
pilot-mcp <cmd> Delegate to underlying pilotctl

Common pilotctl commands (auto-delegated):
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.11",
"version": "0.2.12",
"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.11",
"version": "0.2.12",
"websiteUrl": "https://pilotprotocol.network",
"packages": [
{
"registryType": "npm",
"identifier": "pilotprotocol-mcp",
"version": "0.2.11",
"version": "0.2.12",
"transport": { "type": "stdio" }
}
]
Expand Down
57 changes: 53 additions & 4 deletions src/daemon-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,69 @@ export function daemonSocketPath() {

export async function execPilotctl(args, opts = {}) {
const bin = pilotctlBinaryPath();
const timeoutMs = positiveNumber(opts.timeoutMs, 'timeoutMs');
const maxBufferBytes = positiveNumber(opts.maxBufferBytes, 'maxBufferBytes');
return new Promise((resolve, reject) => {
const child = spawn(bin, args, {
stdio: opts.capture ? ['pipe', 'pipe', 'pipe'] : 'inherit',
env: { ...process.env, ...(opts.env ?? {}) },
});
let settled = false;
let killTimer;
const timer = timeoutMs === null ? null : setTimeout(() => {
if (settled) return;
settled = true;
const error = new Error(`pilotctl timed out after ${timeoutMs}ms`);
error.code = 'ETIMEDOUT';
child.kill();
killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
killTimer.unref?.();
reject(error);
}, timeoutMs);
timer?.unref?.();
let stdout = '';
let stderr = '';
let outputBytes = 0;
if (opts.capture) {
child.stdout.on('data', (b) => { stdout += b.toString(); });
child.stderr.on('data', (b) => { stderr += b.toString(); });
const append = (channel, chunk) => {
if (settled) return;
outputBytes += chunk.byteLength;
if (maxBufferBytes !== null && outputBytes > maxBufferBytes) {
settled = true;
if (timer) clearTimeout(timer);
child.kill();
reject(new Error(`pilotctl output exceeds ${maxBufferBytes} bytes`));
return;
}
if (channel === 'stdout') stdout += chunk.toString();
else stderr += chunk.toString();
};
child.stdout.on('data', (chunk) => append('stdout', chunk));
child.stderr.on('data', (chunk) => append('stderr', chunk));
// A short-lived pilotctl may finish before Node flushes stdin. Linux
// reports that normal close as EPIPE; without a listener it becomes an
// uncaught process error even though the child's exit status and output
// are already authoritative.
child.stdin.on('error', (error) => {
if (error?.code !== 'EPIPE') reject(error);
if (error?.code !== 'EPIPE' && !settled) {
settled = true;
if (timer) clearTimeout(timer);
reject(error);
}
});
child.stdin.end(opts.input ?? '');
}
child.on('error', reject);
child.on('error', (error) => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
reject(error);
});
child.on('close', (code) => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
if (killTimer) clearTimeout(killTimer);
if (opts.capture) {
resolve({ code: code ?? 0, stdout, stderr });
} else {
Expand All @@ -101,6 +143,13 @@ export async function execPilotctl(args, opts = {}) {
});
}

function positiveNumber(value, name) {
if (value === undefined || value === null || value === '') return null;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${name} must be a positive number`);
return Math.floor(parsed);
}

export async function pilotctlJSON(args) {
// Wrapper that adds --json and parses the response, with a clear error if
// pilotctl is missing or daemon isn't reachable.
Expand Down
54 changes: 54 additions & 0 deletions src/doctor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import process from 'node:process';
import { existsSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

import { daemonHealthy, pilotctlBinaryPath } from './daemon-bridge.js';
import { detectHarnesses } from './setup/detect.js';
import { VERSION } from './version.js';

export async function runDoctor(flags = {}, options = {}) {
const home = options.home ?? homedir();
const write = options.write ?? ((value) => process.stdout.write(`${value}\n`));
const report = {
version: VERSION,
runtime: runtimeCheck(),
daemon: { healthy: await daemonHealthy() },
management: managedCheck(join(home, '.pilot', 'managed', 'enterprise-control.json')),
harnesses: (await detectHarnesses()).map(({ id, name }) => ({ id, name })),
};
report.ok = report.runtime.ok && (report.management.attached ? report.management.secure : true);
if (flags.json) {
write(JSON.stringify(report));
} else {
write(`Pilot adapter ${report.version}`);
write(`Runtime: ${report.runtime.ok ? report.runtime.path : report.runtime.error}`);
write(`Daemon: ${report.daemon.healthy ? 'reachable' : 'not reachable'}`);
write(`Management: ${report.management.attached ? (report.management.secure ? 'attached (owner-only)' : report.management.error) : 'not attached (unmanaged pass-through)'}`);
write(`Harnesses detected: ${report.harnesses.map((entry) => entry.id).join(', ') || 'none'}`);
}
if (!report.ok) process.exitCode = 1;
return report;
}

function runtimeCheck() {
try {
return { ok: true, path: pilotctlBinaryPath() };
} catch (error) {
return { ok: false, error: error.message };
}
}

function managedCheck(path) {
if (!existsSync(path)) return { attached: false, secure: true };
try {
const stat = statSync(path);
if (!stat.isFile()) return { attached: true, secure: false, error: 'attachment is not a regular file' };
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
return { attached: true, secure: false, error: 'attachment permissions are not owner-only (0600)' };
}
return { attached: true, secure: true };
} catch (error) {
return { attached: true, secure: false, error: error.message };
}
}
Loading