Skip to content

JSON and MCP

berth check --json and the dashboard's GET /api/state return the same report. Additive changes keep the shape backward compatible; removals or type changes are called out in the changelog.

The report

ts
type State = 'ok' | 'idle' | 'stale' | 'orphan' | 'unmanaged' | 'squatter' | 'conflict' | 'drift';

interface CheckReport {
  version: string;               // berth version
  generatedAt: string;           // ISO time
  cacheAgeMs: number;            // age of the truth snapshot used
  host: { platform: string; user: string; dockerAvailable: boolean; colima: boolean };
  policy: PolicySummary;         // scheme, pools, reserved, shared, projects[] with base
  ports: PortRecord[];           // one per leased, declared, shared or bound port of interest
  sessions: SessionRecord[];
  summary: { ports: number; attention: number; liveSessions: number; byState: Record<State, number>; ignored?: number };
}

interface PortRecord {
  port: number;
  state: State;
  project?: string; worktree?: number; role?: string;
  kind?: 'block' | 'dynamic' | 'declared' | 'shared';
  decoded: { P: number; W: number; R: number } | null;   // null outside the block range
  lease: Lease | null;
  live: { holder: string; pid?: number; container?: string; cwd?: string; sessionId?: string; tool?: string; proxy?: boolean; project?: string } | null;
  evidence: string[];            // one line per source, e.g. "docker: container x, compose y, working_dir ~/p"
  advisory: { text: string; command?: string } | null;
  url?: string; age?: string;
  attribution?: 'lease' | 'evidence';   // how project/worktree/role is known; omitted when neither applies
}

interface Lease {
  port: number; project: string; worktree: number; role: string;
  kind: 'block' | 'dynamic' | 'declared' | 'shared';
  owner: { session_id?: string; tool: 'claude-code' | 'human' | 'macos' | 'unknown'; pid?: number; pid_start?: string };
  cwd: string; created: string; expires?: string | null; note?: string; ended?: string | null;
}

interface SessionRecord {
  id: string; short: string; tool: string; pid?: number; started?: string; ended?: string | null; alive: boolean;
  project?: string; worktree?: number; cwd?: string; cwdExists: boolean;
  leases: number[]; worst: State; howWeKnow: string[];
}

Other commands: berth ls --json is ports[]; berth who N --json is one PortRecord; berth env --json is { project, P, W, worktree, block, ports: [{ role, R, port, env }] }; berth env --compose-override --json is { project, file, override, composeFile, composeCommand, mapping: [{ service, currentHost, newHost, containerPort, role }], warnings: [{ service, container, ports: [{ current, new, containerPort }], message, recreateCommand, cautions: string[] }] }warnings is empty when every mapped service's current holder is unbound or carries compose labels; berth project add --json is the registered entry with created and block.

MCP tools

berth mcp speaks JSON-RPC 2.0 over stdio (newline-delimited) and supports initialize, ping, tools/list and tools/call. The plugin registers it automatically; by hand: claude mcp add --scope user berth -- berth mcp.

ToolInputReturns
berth_check{ text?: boolean }the report as JSON, or the human summary
berth_who{ port }the berth who text followed by the JSON record
berth_ls{ project?, all? }ports[]
berth_claim{ role? | extra? | port? | dynamic?, cwd?, project?, note?, session? }the claim result; isError when refused
berth_release{ port? | all?, session? }{ released, refused }
berth_env{ cwd? }the berth env --json object

Only self-scoped operations are exposed; there is no tool for free, hooks, prune or init.

Released under the MIT licence.