npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@black942026/harness-acp-bridge-mcp

v0.1.12

Published

Stdio MCP proxy for the harness-acp-bridge HTTP server: exposes its operations as tools without owning harnesses, state, or scheduling.

Readme

@black942026/harness-acp-bridge-mcp

Stdio MCP proxy for the harness-acp-bridge server.

It exposes the server's HTTP operations as MCP tools. It owns no harness, state, or scheduling: most tools are a single POST /api/<name>, while send_message/wait submit (or resume) once and then poll message_status until the message is terminal. If no compatible server is listening on the configured loopback address, the proxy starts the installed server package as a managed background service and reuses it. Harness processes, scheduling, persistence, SSH/Docker launch, template storage, and authentication all remain the server's responsibility.

MCP client --stdio JSON-RPC--> this proxy --HTTP POST /api/<name>--> bridge server

send_message keeps the server's asynchronous contract but waits for the terminal result in the same tool call, exactly like the Pi client. A second tool, wait, resumes waiting for a known session_id/message_id without resubmitting.

Tool-call timeout: the proxy sends no progress notifications (so no output body leaks through them). A send_message/wait is therefore one long-running tool call; configure the MCP client's per-call timeout to cover the full turn, and HARNESS_ACP_BRIDGE_TIMEOUT_MS to cover a single HTTP request.

Install

npx -y @black942026/harness-acp-bridge-mcp

Register it as a stdio MCP server; it has no CLI arguments or admin flags.

Configuration

Environment variables win over the optional JSON file (config.example.json, selected with HARNESS_ACP_BRIDGE_MCP_CONFIG). The only accepted JSON keys are baseUrl, timeoutMs, autoStart, serverEntry, configPath, and startupTimeoutMs.

| Variable | Default | Purpose | | --- | --- | --- | | HARNESS_ACP_BRIDGE_URL | http://127.0.0.1:8787 | Base URL of the server. http/https only; no credentials, query, or fragment. | | HARNESS_ACP_BRIDGE_TIMEOUT_MS | 120000 | Per-request HTTP timeout. | | HARNESS_ACP_BRIDGE_AUTOSTART | true | Start a local server when the configured one is unreachable. | | HARNESS_ACP_BRIDGE_SERVER_ENTRY | — | Explicit server module (or path / file: URL) to import when starting a local server. Must export HarnessHttpServer/loadConfig (the server package index), not the hacpd CLI (dist/main.js). A relative path is resolved against the proxy's startup cwd. When set it is final: it is never silently ignored, and no PATH/dependency fallback is probed. | | HARNESS_ACP_BRIDGE_CONFIG | — | Server YAML config path passed to the started server (also honored as a client setting). | | HARNESS_ACP_BRIDGE_STARTUP_TIMEOUT_MS | 30000 | How long to wait for a started server to answer ping. | | HARNESS_ACP_BRIDGE_MCP_CONFIG | — | Optional JSON settings file. | | HARNESS_ACP_BRIDGE_CLIENT_DIR | ~/.harness-acp-bridge-client | Client-owned directory for the start lock, server log, and pid registry. |

Local-server entry selection

When a local server must be started, the entry is chosen in this order:

  1. HARNESS_ACP_BRIDGE_SERVER_ENTRY / JSON serverEntry — an importable server module. Explicit and final; a wrong value fails with startup_failed and is never silently skipped.
  2. hacpd found on PATH (a fixed-name lookup, never a caller-supplied command). A standard npm global install exposes a .bin symlink to the package's dist/main.js; the proxy resolves the symlink, and — because the CLI runs the server in-process and does not read stdin — starts it as node --import <bundled watchdog> <hacpd> so the child PID is the answering server PID (exact ownership proof) and a supervisor exit (including SIGKILL) reaches the server as a stdin EOF, stopping it instead of orphaning it. The CLI's own module resolution is preserved.
  3. Otherwise the installed @black942026/harness-acp-bridge-server dependency's module entry.

A hacpd that is not a Node script (any other shebang, e.g. a shell wrapper) cannot carry the in-process watchdog or give an exact PID, so it is refused with startup_failed (and the log says so) rather than run unobserved. A found hacpd that starts but never answers ping, or answers incompatibly, likewise fails hard — the resolver never switches to the next candidate after a startup failure. PATH entries that are empty mean the cwd and relative entries are resolved against the client's startup cwd (never the supervisor's); an unset PATH disables the hacpd probe entirely (it is never read as the cwd), so a project-local hacpd is never run by accident.

The proxy never accepts a command or argv pass-through, so a caller cannot make it launch anything but the configured server module or the fixed hacpd PATH lookup through the managed path. The internal launch variables (..._SERVER_MODULE / ..._SERVER_COMMAND) are cleared and exactly one is set per launch, so an inherited value cannot hijack the spawn.

Timeout keys (MCP ⇄ Pi)

| Concept | MCP | Pi extension | | --- | --- | --- | | Per-request HTTP timeout | timeoutMs / HARNESS_ACP_BRIDGE_TIMEOUT_MS | requestTimeoutMs / HARNESS_ACP_BRIDGE_TIMEOUT_MS | | Startup wait (until ping answers) | startupTimeoutMs / HARNESS_ACP_BRIDGE_STARTUP_TIMEOUT_MS | startupTimeoutMs / HARNESS_ACP_BRIDGE_STARTUP_TIMEOUT_MS |

Both clients share HARNESS_ACP_BRIDGE_URL / _AUTOSTART / _SERVER_ENTRY / _CONFIG / _STARTUP_TIMEOUT_MS / _TIMEOUT_MS, all with the same precedence: environment > JSON > default. Each keeps its own default timeout value. The Pi-only interactionTimeoutMs / pollIntervalMs have no shared environment variable.

Local server lifecycle

  1. ping the configured baseUrl. A server answering ping with protocol_version: 4 is reused as-is. An older protocol (e.g. a still-running protocol_version: 3 daemon, whose create_session still referenced a template) is treated as incompatible and rejected, but it is never restarted, replaced, or killed.
  2. If the address answered but is not a compatible bridge server (an error envelope, a different protocol version, or a non-JSON listener), the proxy reports incompatible_server and starts nothing — a foreign service is never treated as "absent".
  3. If nothing is listening and autoStart is on, the proxy starts the server only for a loopback address. A non-loopback baseUrl is never spawned, locally or over SSH.
  4. Starts are serialized with a per-address lock in the client directory, then re-checked, so two clients racing to start the same instance converge on one server (the server's own state_dir lock and port binding are the final backstop).
  5. The started server runs as a child of a detached logging supervisor. The supervisor holds the server's real stdout/stderr fds as pipes and appends only redacted bytes to the log, so it survives the proxy and is never killed by it — it cannot tear down another client's sessions. <client dir>/servers.json records the actual server pid, the supervisor pid, and the log path. Ownership is proven, not guessed: the proxy spawns the supervisor with a fresh nonce and claims the server only when the answering ping pid equals the supervisor's recorded server_pid and the private proof file carries that nonce. A peer that won the race is reused; a foreign listener is never treated as absent and never killed.
  6. shutdown/reload release only the client's own transport. They never close server sessions or stop a reused instance.

Tools

Eleven operations plus search_template, create_template, and wait:

| Tool | Behavior | | --- | --- | | ping | Liveness, protocol version, PID, uptime, live session count, config fingerprint. | | search_template | Find launch templates by exact name, query (case-insensitive substring of name/harness/description), and/or harness; returns each match in full. Starts nothing. | | create_template | Durably register a launch template from {template: {...}}. A client alias for the server's register_template — there is no POST /api/create_template. Duplicate names are rejected (template_exists) and never overwritten. Starts nothing. | | create_session | Create a session-owned harness from explicit parameters: required model_id and cwd, plus optional harness, command/args/env, thinking_level, permission_mode, target/remote_host, runtime, and the Docker fields (docker_image or docker_id, container_policy, login_dirs, mounts, ports, host_network). There is no template parameter. | | close_session | Stop the harness and close the session (idempotent). | | authenticate | Start or resume the harness authentication flow; the proxy elicits every question and polls until terminal. | | auth_status | Return the current authentication workflow shape, or probe harness authentication when no flow exists. | | set_session_options | Change model_id / thinking_level / permission_mode on an idle session. | | send_message | Submit a prompt and synchronously wait for the terminal result in this call. | | cancel_message | Cancel the active message and wait for it to settle. | | message_status | State, terminal flag, pending questions, unsupported events, errors, and result. | | answer_question | Answer a reverse request by session_id/request_id (option_id or object content); message_id is omitted for session-auth questions. | | wait | Resume waiting for a known session_id/message_id; never resubmits. |

create_template/search_template carry MCP annotations that reflect reality: both have openWorldHint: false (neither reaches a harness); search_template is readOnlyHint: true, and create_template is a non-destructive server write (readOnlyHint: false, destructiveHint: false, since a duplicate name is rejected rather than overwritten). delete_template is deliberately not exposed as a tool.

Launch templates

search_harness is removed; select a harness via launch templates or explicit session parameters. The server stores launch templates (model/thinking/instruction/command/argv/env/Docker defaults), but templates are information only: create_session never references one. The intended workflow:

  1. The user names or describes a template. Call search_template with an exact name when known, otherwise with query/harness. If several match, ask the user which one before proceeding.
  2. Read the returned template's full content — the proxy never guesses it.
  3. Map non-null model → model_id, thinking → thinking_level; user overrides win. Ask only for a missing model or cwd. No model/thinking candidate lists are required in server YAML.
  4. Follow special_instructions throughout; the calling LLM executes before_start before startup and after_start after successful startup and any required authentication. These are natural-language prompts, not proxy/server hooks, and remain subject to user approval and safety rules. Report failures/unavailable tools rather than claiming actions ran.
  5. Map Docker network_mode to session host_network (host → true; none/proxy → false) and pass the returned env. Drop name/description/model/thinking/network_mode/proxy_port/ before_start/after_start/special_instructions and inapplicable fields, then call create_session with only allowed explicit fields. Omit invalid nulls (local remote_host, missing cwd) and re-check runtime/target combinations. The proxy never merges a template or executes its instructions.

To create a template, call create_template with a {template: {...}} object. Only the fields the server can register are accepted: name (required), harness (required), and optional description, model, thinking, before_start, after_start, special_instructions, runtime, target, remote_host, permission_mode, command, args, env, cwd, docker_image, container_policy, login_dirs, mounts, ports, network_mode, proxy_port. Unknown fields are rejected. Prefer collecting and confirming the values with the user first; the acp-create-template skill (published with the Pi extension package, and portable to MCP hosts) documents that interview. Registering a template never creates or starts a session or executes instruction fields. All five new fields are nullable/optional; model/thinking must be nonblank when set. Existing templates load with null defaults. Remove the entire obsolete harnesses section from server YAML; all built-in adapters are available without declarations. Select the harness via templates or explicit session parameters; omitted harness defaults to Codex (overridable with default_harness). No YAML file is needed when all service settings use defaults.

Docker network modes

Templates use network_mode: none | host | proxy, not the old host_network (rejected). none is ordinary bridge without proxy values; host enables host network without proxies or published ports. proxy requires only proxy_port (1–65535): registration sets identical lowercase http_proxy/https_proxy to http://host.docker.internal:PORT, and no_proxy to localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.local,.internal. It replaces stale proxy aliases while keeping unrelated env; the service need not be Xray. Pass this returned env to create_session, map mode to its host_network boolean, and omit the metadata network_mode/proxy_port. Confirm host-address/port reachability from the target Docker container and HTTP-client/CIDR support; do not infer it from a successful env round-trip. Re-register old templates; no old-template migration is implemented.

Authentication

authenticate takes only { session_id }; the proxy drives the flow and never lets the caller pick a method or a token. It elicits the server-provided method_id selection (the candidates, including the Not supported in harness_acp_bridge ones, are shown in the question message; only supported ids are offered), then elicits each follow-up question: the CodeBuddy “open this URL, then confirm” (opened: boolean), an Agy token, a Codex device code, and so on, in as many rounds as the server sends. Confirming a URL is not success — polling continues until the flow is terminal (ready/failed/cancelled; a rate limit is authentication_rate_limited). Each answer is sent as a session-scoped answer_question (no message_id) after re-checking the request_id. Without elicitation the tool returns waiting_input with the full questions for a manual answer_question followed by authenticate. Aborting only detaches the local wait; a decline/cancel/timeout is a real answer. create_session (and any op) that needs login returns a structured authentication_required error whose detail keeps session_id and next_action — the model then calls authenticate; the proxy never auto-authenticates.

Reverse requests

While send_message/wait runs, a reverse permission or information request is answered with an MCP elicitation when the connected client advertised the capability: offered permission options are mapped back to their exact optionId, and nothing is approved by default (a decline maps to reject, a cancel to cancel, and an unoffered selection is refused). When the client cannot elicit, the tool returns waiting_input with the original questions so the model can call answer_question and then wait. Aborting wait only stops local waiting and leaves the remote message running; a later wait can resume it. Aborting send_message still cancels the remote message, matching the Pi client. Server timeout, question-answer, and JSONL error policies are unchanged.

There is no incremental-output tool (live_output is gone). send_message/wait return only the terminal status (whose result is the server's final answer); the proxy sends no progress notifications, so no output body leaks through them.

Each success returns the server's data object as JSON text. A failure returns an MCP tool error (isError: true) whose text is the envelope { "ok": false, "error": { "code", "message", "detail" } }, preserving the server error code and its detail (including a structured authentication_required).

Privacy

The proxy is a thin client: the real HTTP response is what it returns. It only sanitizes the copies it persists, and only common credential shapes.

  • The detached bootstrap is a logging supervisor: it launches the actual server with stdout and stderr as pipes (never the log file) and redacts both streams — independently, with bounded cross-chunk state — before appending to server-<addr>.log. Because the real fds are pipes, every output path is covered: console, stream.write, fs.writeSync(1/2), fs.writev, native modules, dynamic-import and runtime diagnostics. A JS-level write patch cannot see fd-level writes; the pipe boundary can.
  • Redaction lives entirely in the detached supervisor, never in the client, so it keeps working after the client exits while the server stays alive. At EOF both held tails are flushed (masked) before the log fd is closed, and existing log bytes are never rewritten (append only).
  • A redactor or disk error never falls back to raw bytes: a redactor exception drops the chunk, and a disk write failure stops the owned server and exits non-zero rather than retrying forever or letting the server block on a full pipe.
  • logTail/registry read-time masking is defense in depth only, never the security boundary.
  • servers.json is sanitized before its atomic write; the top-level map keys are base URLs (already credential-free) and stay intact so registry and ownership lookups keep working. The log file is mode 0600 in a mode-0700 client directory, and the launcher proof file is mode 0600.
  • Rules are the canonical server files copied verbatim: src/redact-core.ts and src/redact.ts are byte-identical to server/src/redact/redact-core.ts (sha256 b07ef16e39483b008c0f03e3da05dc194c87fa93080b53accf34317586d2a6c8) and server/src/redact/redact.ts. The build emits dist/redact-core.js + dist/redact.js, so nothing is hand-ported. To re-sync: copy the two files over src/ and rebuild.
  • A parity harness (npm run parity) loads this package's build and the sibling server build and asserts byte-exact output for both the non-streaming and the streaming APIs across a representative battery (report: /tmp/redaction-parity-mcp.log). The Pi extension pins itself against the same server build with its own copy, so all three agree transitively.
  • Unknown/unrecognized text — including counters like max_tokens and ordinary CJK/emoji text — is preserved verbatim. This is not a guarantee that every unknown secret is removed.

Develop

npm ci
npm run check                 # clean + build + typecheck + tests
npm pack --dry-run

The package and its tests do not import or require the Pi extension checkout or dependencies. npm run check starts a real server and its fake ACP harness (no paid model, Docker, or SSH) and drives it through real stdio MCP clients with and without elicitation capability, including autostart, reuse, answer+wait, cancel, the authentication flow (unauthenticated create_session → method selection → URL/token answer → auth_status success), and the template flow (create_template → search_template by name/query/harness → expand the allowed fields into an explicit create_session with an inherited-then-overridden cwd and command/args/env, plus duplicate/invalid/unknown templates and client-side rejection of a missing cwd, a template reference, or launch internals). Harness commands come from the explicit command/args/env or the execution target's PATH, so the integration suite installs PATH wrappers around the fake harness for the template-less cases. Integration and lifecycle suites resolve the in-repo server build (../server/dist) first and fall back to the installed package; because the client requires protocol_version: 4, they are skipped until the server is rebuilt with the new contract. Point ACP_BRIDGE_SERVER_DIR at a server checkout (defaults to ../server).

License

MIT