Skip to content

Latest commit

 

History

History
573 lines (517 loc) · 34.2 KB

File metadata and controls

573 lines (517 loc) · 34.2 KB

agent-connector — Architecture

Write your MCP server + hooks once. agent-connector detects every AI-agent platform on the machine, renders the right config in each one's native dialect, installs/syncs/uninstalls them, and gives you default, platform-independent per-tool token telemetry — the metric MCP developers actually want.

This document is the authoritative design. It is grounded in the understand-phase report (docs/research/understand-report.md), which reverse-engineered context-mode's proven 15-platform adapter layer and the real installed config formats across Claude Code, Codex, Cursor, OpenCode, VS Code/JetBrains Copilot, Gemini, Warp, Hermes, and more.


1. Problem

Every AI-agent host re-invents the same two integration surfaces — MCP server registration and lifecycle hooks — with mutually incompatible config files, root keys, formats (JSON / JSONC / TOML / YAML / exported TS-or-Python functions), transports, scopes, and event vocabularies. A developer who wants reach must hand-author and maintain N dialects, N hook adapters, and N install flows, then chase each platform's quirks (Cursor silently fails on a wrong root key; VS Code uses servers not mcpServers; Codex uses TOML [mcp_servers.x]; OpenCode wants an exported TS plugin function, not a hook table). context-mode paid this cost in full for a single server. agent-connector generalizes that work into a reusable framework.

Second, no host reports per-tool token usage back to an MCP server (the MCP spec has no usage on CallToolResult). So "how much context does installing my server actually cost, everywhere?" — the question MCP devs care most about — is unanswerable today. agent-connector answers it by measuring the server's own bytes and tokenizing them locally, identically across all hosts.

2. Two pillars

  1. Single-API multi-platform deployment. One declarative + programmatic defineConnector({...}) → adapters render it into each platform's native MCP registration and hook config; one CLI installs/syncs/uninstalls everywhere.
  2. Default per-MCP token telemetry. Platform-independent, local-first, privacy-preserving (aggregate counts, never content). On by default, opt-out granular.

3. Operating model — home-dir-centric, single binary, per-project data

This is the baseline the user mandated (oh-my-claudecode style), refined after critical review. Three rules:

R1 — One home binary; everything routes through it

The framework installs one runtime in the home root:

~/.agent-connector/                 (override: AGENT_CONNECTOR_DATA_DIR)
  bin/agent-connector               single binary — CLI + universal hook entrypoint + telemetry runtime
  connectors/<id>/connector.json    each registered connector's resolved definition
  telemetry.db (or telemetry.ndjson) shared store, rows keyed by project — see R3
  backups/                          timestamped settings backups before each mutation
  logs/

Every platform config we write is a thin pointer back to this one binary:

  • a hook command is agent-connector hook <platform> <event> --connector <id> (mirrors context-mode's proven context-mode hook <platform> <name> dispatch);
  • an MCP server entry runs the connector's server, optionally wrapped by agent-connector serve --connector <id> -- <real server cmd> so telemetry is captured with zero work from the dev.

Because the pointers reference one stable home path, updating that single binary updates behavior in every platform at once — exactly the requested ergonomic.

Critical adjustment #1 — stable path, managed update (NOT silent auto-update). A forced/silent auto-update of a single binary means one bad release breaks every project × every platform simultaneously (maximum blast radius, lost reproducibility) — the npx pkg@latest failure mode. So: pointers reference a stable path (~/.agent-connector/bin/agent-connector), never a versioned cache dir (this also sidesteps the whole class of bug that forces context-mode's cache-heal SessionStart hook). Updates are explicit/managed (agent-connector upgrade, channel = stable|latest), with an optional per-project version pin override. One place to update — without global instant breakage.

R2 — "Home-centric" cannot be absolute; platforms force project/global scopes

Cursor, VS Code Copilot, etc. require config files at platform-mandated locations (.cursor/mcp.json, .vscode/mcp.json, ~/.codex/config.toml, …). So:

Critical adjustment #2. "Home-based" means the binary + shared telemetry store live in home as the single source of truth; the framework still writes the minimal native pointer config wherever each platform mandates it. Those pointers exec the one home binary — which is how "single-binary update" actually propagates to hosts that never read ~/.agent-connector.

Native config files (settings.json, config.toml, mcp.json) are never relocated by AGENT_CONNECTOR_DATA_DIR — only framework-owned state is. (Generalized from context-mode's resolveContextModeDataRoot / issue #649.)

R3 — Per-project data, keyed by project identity, retained under home

Critical adjustment #3. Telemetry/state is keyed by a stable project identitygitRemote || normalizedAbsPath, hashed — and stored under the home data-root (default), not by code location. This survives git clean, isn't committed, and lets multiple platforms opening the same project share one store (the cross-agent shared-DB mechanism). In-repo storage is opt-in. Concurrent writers (several hosts at once) are handled by append-atomic NDJSON (MVP) or SQLite WAL (upgrade). Data is partitioned by project_key column → both per-project retention and cross-project rollups.

R4 — Native Windows correctness, no symlink installs

Single binary must resolve home via the right per-OS dir (%USERPROFILE% / %APPDATA% / XDG / homedir()), no POSIX-only assumptions, no symlink-based installs. The Windows-safe spawn/quoting helpers (buildNodeCommand / parseNodeCommand, fixing context-mode bugs #369/#372/#548/#738) are mandatory.

4. The common abstraction model

Distilled from the union of platform behaviors (report §3).

  • ServerDef — transport-polymorphic server descriptor (stdio | http | sse | ws; command/args/env/cwd or url/headers/auth; tools, timeoutMs, enabled). Adapters render it into each dialect; the root key is a per-adapter constant (mcpServers | servers | mcp | mcp_servers), as are field-name differences (cwd vs working_directory, env vs environment, scalar command vs command:[]).
  • Transport capability set — an adapter may reject/downgrade a transport it can't honor and report it (never crash).
  • Scope — normalized ordered enum { system, user, project, profile, managed }; each adapter maps to a concrete path + knows precedence. Default install scope = user; --scope project opt-in.
  • Normalized lifecycle eventsSessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop, Notification, PermissionRequest, PostToolUseFailure, SubagentStart, SubagentStop, PostCompact (13 canonical events — the last five are newer additions; hosts without a native analog mark them unsupported in capabilities and the install reports a skip-warn, never a silent drop). Mapped to each platform's names (PreToolUseBeforeTooltool.execute.beforepre_tool_call). Normalized payload { toolName, toolInput, toolOutput?, isError?, sessionId, projectDir?, raw }; normalized response { decision: allow|deny|modify|context|ask, reason?, updatedInput?, additionalContext?, updatedOutput? }. The union is a floor, not a ceiling: host-only events (Claude Code alone ships 30) are reachable per platform via the platforms.<id>.nativeHooks passthrough — raw payload in, verbatim JSON reply out, exit 0 only; adapters that set supportsNativeHooks wire it and the rest skip-warn. An event is promoted into the union once ≥3 hosts ship a native analog (TaskCreated/TaskCompleted first candidates). Full contract: llms-full.txt §2.3.
  • Hook I/O paradigm taxonomy (the deepest divergence — exactly three):
    • json-stdio (18) — Claude Code, Codex, Cursor, VS Code Copilot, JetBrains Copilot, Copilot CLI, Gemini CLI, Qwen, Kiro, Kimi, Crush, Goose, Hermes, Droid (Factory), Antigravity, Antigravity CLI, Continue, Amazon Q. One universal hook entrypoint binary reads host JSON, the adapter normalizes it, the dev's handler runs, the adapter formats the reply.
    • ts-plugin (8) — OpenCode, MiMoCode, Kilo CLI, Kilo, OMP, NemoClaw, OpenClaw, Amp. Framework generates an exported plugin module importing the dev's handler.
    • mcp-only (9) — Warp, Roo Code, Cline, Trae, Zed, Codebuff, Mux, Pi, Windsurf. No hook layer; install only the MCP server; detection surfaces "hooks unavailable here."
  • PlatformCapabilities flags (preToolUse, postToolUse, preCompact, sessionStart, canModifyArgs, canModifyOutput, canInjectSessionContext) — the single-API layer queries these and degrades gracefully.
  • Detection — two layers: install-time platform detection (which hosts are installed: config-dir + marker files) and runtime host detection (which host is executing this hook now: env-var markers → config-dir → clientInfo). Generalized from context-mode's registry.ts + detect.ts (incl. fork-before-parent order & foreign-env scrubbing).
  • Escape hatch — every adapter accepts platforms.<id>.extra passthrough so a dev reaches platform-exclusive features the core doesn't model. Thin universal core + fat per-adapter tail.
  • memory (the fourth content surface) & the AGENTS.md standard — standing guidance declared once (memory: [{ name?, description?, content }]) and written by each supporting adapter as a marker-fenced managed block into the memory/rules file that host actually reads. Unlike commands / skills / subagents (files we wholly own), memory edits a SHARED, user-authored file — so every write goes through one dependency-free engine (core/managed-block.ts): markers <!-- agent-connector:begin <connectorId>/<name> hash=<sha256-12> --><!-- agent-connector:end <connectorId>/<name> --> plus a one-line do-not-edit notice; the blockId-on-the-marker makes multi-connector coexistence safe, and the hash (sha256-12 over the CRLF→LF-normalized, trimmed inner content) gives O(1) idempotence (unchanged → skip) and edit detection (inner hash ≠ recorded ⇒ user edited ⇒ warn-and-leave; overwrite only under install --force after a timestamped backup). Replacement is in-place — zero bytes outside the marker pair ever change; the scanner is line-anchored, CRLF-preserving, BOM-safe, and fence-aware. AGENTS.md-first policy (grammar v1): 29/35 hosts read the open AGENTS.md standard, so project scope targets <projectDir>/AGENTS.md — with exclusive/first-match readers PROBED so the block lands in the file the host will actually read (zed's first-match candidate list, warp's WARP.md priority, hermes' .hermes.md, opencode's CLAUDE.md fallback, codex's AGENTS.override.md; openclaw maps both scopes to its agent workspace) — and user scope the host's documented global memory file: AGENTS.md where one exists, else the host's own file (~/.qwen/QWEN.md, goose .goosehints, ~/.copilot/copilot-instructions.md, kilo/roo/kiro rules-dir agent-connector.md), else skip-warn. The two exceptions follow their own official docs: claude-code → CLAUDE.md ("Claude Code reads CLAUDE.md, not AGENTS.md" — HTML-comment markers are stripped from Claude's context, invisible to the model yet parseable by us; opt-in memory.mode: "agents-import" manages the documented @AGENTS.md import as a ref-counted _shared bridge block instead) and gemini-cli → GEMINI.md (AGENTS.md only when context.fileName opts in). We never edit host settings to make AGENTS.md readable — probe and respect only. Install order: memory last among content surfaces, removed FIRST on uninstall; uninstall excises every block under the <connectorId>/ marker prefix (markers in the file are the source of truth; the connectorDir(id)/memory-state.json ledger adds created-file deletion rights + doctor diagnostics: block present / hash intact / user-edited / file missing). Full contract: llms-full.txt §2.4.
  • statusline (handler surface) — a HUD / status-line declared once (statusline?: StatuslineDef) and driven by a developer-supplied render function, not content. Singular (one per connector, not an array). Interface:
    interface StatuslineDef {
      name?: string;          // kebab-case id; default "statusline"
      description?: string;
      options?: StatuslineOptions;
      render: (ctx: StatuslineContext) => string | Promise<string>;
      hosts?: Partial<Record<PlatformId, {
        render?: (ctx: StatuslineContext) => string | Promise<string>;
        options?: StatuslineOptions;
      }>>;
    }
    StatuslineContext carries { host, connectorId?, sessionId?, cwd?, model?{id?,displayName?}, cost?{totalUsd?}, context?{usedTokens?,maxTokens?,percent?}, transcriptPath?, raw } — fields the host doesn't provide are undefined; raw is the host's verbatim payload. StatuslineOptions includes host-mapped knobs (refreshInterval, respectUserColors, hideContextIndicator) and framework-enforced maxLines; adapters write only knobs their host actually supports. SDK helper defineStatusline({ render }) (typed identity) plus types StatuslineDef / StatuslineContext are exported from the package root and /sdk. Deployment (command-driven v1 = antigravity-cli + claude-code + qwen-code): PlatformCapabilities.supportsStatusline gates it (read as ?? false); every other adapter emits the standard skip-warn, never a silent drop. Capability metadata exposes statuslineMode and option support so SDK users can see whether a host is command-stdin and which options can be written. On claude-code, install registers settings.json.statusLine = { type: "command", command: "<homeBin> statusline claude-code --connector <id>", refreshInterval? }; on qwen-code it registers the nested settings.json.ui.statusLine equivalent and may pass refreshInterval, respectUserColors, and hideContextIndicator; on antigravity-cli it writes the agy command statusline shape. Hosts whose only status UI is a built-in preset (for example a host-owned compact/verbose mode with no connector-owned command entrypoint) are intentionally not modeled as a render(ctx) statusline yet. All supporting hosts use the refcounted ownership ledger: set-if-absent, never clobber a status-line setting agent-connector doesn't own (skip-warn + manual-edit hint), and uninstall removes only when last-owner ∧ value-unchanged ∧ prior-absent. statusLine remains a reserved key for the modeled statusline surface — raw configPatch targeting it throws ConnectorConfigError pointing at statusline where configPatch is available. Runtime is fail-safe: any error (throwing render, unknown connector, malformed stdin) → exit 0 / empty stdout; a HUD must never wedge the host. Doctor adds a dedicated statusline wired check (ok / present-but-not-ours / missing). CLI verb: agent-connector statusline <platform> --connector <id> (internal, like hook/serve).
  • actions (dispatch backbone) — a connector declares actions?: ActionDef[]. Each ActionDef = { id: string; label?: string; description?: string; icon?: string; placement?: ActionPlacement | ActionPlacement[]; confirm?: boolean | { title?: string; message?: string }; run: (ctx: HostCtx) => ActionResult | void | Promise<…>; hosts?: per-host metadata/run override }. The universal verb agent-connector action <platform> <actionId> --connector <id> loads the connector and invokes run(ctx). Error semantics are USER-TRIGGERED (not fail-safe-silent like hooks/statusline): unknown action id or a throw → exit 1 + stderr. defineAction({ id, run }) is the typed authoring helper (exported from root and /sdk). v1 = universal dispatch plus selected host affordance emitters: install emits host-side affordances on droid, hermes, kiro, nemoclaw, omp, openclaw, pi, warp, and zed; every other host skip-warns. Capability metadata exposes actionInvocationMode (exec, exec-file, manual-hook, paste, plugin-command, or task) and actionAffordanceKind (slash-command, hook-panel, workflow, extension-command, task, etc.) so SDK introspection can distinguish "supported" from "supported how". actions is a real member of the SurfaceName introspection vocabulary; explain() reports those emitter hosts as native with mode/affordance details and the rest as skip-warn; simulate() does not cover actions (an action takes no host payload and has no host-honor verdict — intentional).
  • configPatch — the third (and smallest) escape hatch beside extra and nativeHooks: a declarative, ownership-tracked patch of ONE host-exclusive config key extra cannot reach (extra merges into the native MCP ENTRY / content frontmatter, not sibling top-level settings keys — e.g. Claude Code's env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS). Semantics are FIXED and not configurable: set-if-absent on a single dotted leaf key (segments [A-Za-z0-9_-]+, no array indices, no deep merge, no overwrite, no delete), skip-warn on ANY conflict (present key, drifted value, non-object intermediate — every skip prints the exact manual edit from the required reason + optional docsUrl). Ownership lives in a persisted, refcounted ledger (<dataRoot>/state/config-patches.json, atomic writes): co-owners refcount a shared key; uninstall removes a key only when the LAST owner releases it AND the current value still deep-equals what was written AND the prior state was absent — otherwise the key is left in place with a warn. Doctor reports per-patch ok / drifted / missing / orphaned and never auto-fixes drift. Safety: keys agent-connector already models (hooks*, mcpServers*, statusLine*) are rejected at defineConnector (namespace guard), and each supporting adapter hard-refuses a documented sensitive-key denylist (claude-code: permissions*, allowedTools*/disallowedTools*, apiKey*, awsAuthRefresh/awsCredentialExport, forceLoginMethod/forceLoginOrgUUID, otelHeadersHelper, env.ANTHROPIC_*, env.AWS_*, env.*_PROXY, env.*TOKEN*/env.*KEY*/env.*SECRET*). v1 host scope: claude-code only (supportsConfigPatch); every other adapter reports the standard skip-warn (the supportsNativeHooks precedent) plus the per-patch manual edit. Promotion rules: (a) a second host gains supportsConfigPatch only on demonstrated, genuine connector-facing key demand for that host; (b) a host-exclusive feature graduates from configPatch to a typed cross-host knob only when ≥3 hosts ship an analog — the same bar as hook-event promotion (statusline is the first graduated example: previously a configPatch key, now a reserved first-class surface). Format-preservation requirements for future hosts: VS Code JSONC must use jsonc-parser modify/applyEdits and Codex config.toml must use an anchored section/line edit — core/toml.ts's parse/stringify round-trip destroys comments/ordering and is BANNED for configPatch. Explicitly NOT configPatch targets: statusLine (→ statusline surface; raw configPatch targeting statusLine or statusLine.* throws ConnectorConfigError — declare it via statusline: { render } instead); VS Code inputs arrays and Zed context_servers.<id>.settings — same-file sibling structures coupled to the MCP entry's lifecycle (adapter dialect / extra territory; VS Code inputs doubles as the secret-prompt mechanism). Also out of v1 scope (deferred, documented): TOML hosts (Codex's experimental_use_rmcp_client becomes codex-adapter-internal behavior when remote-MCP support lands), onConflict/force options, array/index paths, secret sourcing, sidecar/side-state files, prereq checks, and sync-removal of patches dropped between connector versions (uninstall/reinstall covers it).

5. What we borrow vs. generalize from context-mode

Borrow (the mechanical spine): the HookAdapter SPI shape, single-source ADAPTER_REGISTRY + matrix test, runtime detection + disambiguators, Windows spawn/quoting helpers, BaseAdapter, the data-root override, the 3-paradigm taxonomy, capability flags, the thunk-based doctor.

Generalize away (context-mode domain logic, must not leak into the framework): the hardcoded "context-mode" identity → becomes the dev-supplied id/entrypoint parameter; baked-in hook scripts → synthesized from the dev's handlers; session / memory / instruction-file / FTS / bytes_avoided logic → deleted from the SPI; context-mode's analytics schema → replaced by our own minimal telemetry schema; the 4-bytes/token heuristic → demoted from default to fallback (a real tokenizer is the default).

6. Telemetry architecture

  • Measure the server's own bytes — the only data identical across hosts. Intercept every tools/call at the server boundary (via agent-connector serve proxy or in-proc middleware). input = params.arguments; output = result.content[] + structuredContent. Also tokenize tools/list schemas once → the fixed "cost of merely defining my tools" per-turn overhead.
  • Default = real tokenizergpt-tokenizer (pure-JS, no native build → Windows/single-binary safe): o200k_base for OpenAI/Codex-family (labeled tokenizer-exact), and the same o200k_base as a documented approximation for every other family (labeled tokenizer-approx; no offline Claude tokenizer ships). Family auto-selected from initialize.clientInfo or modelFamilyHint.
  • Fallback = heuristicchars/4 with content-type multipliers; labeled heuristic so it's never mistaken for exact. Non-text blocks: per-modality formulas, never tokenize base64.
  • Confidence tag on every record: tokenizer-exact | tokenizer-calibrated | tokenizer-approx | heuristic | host-native.
  • Opt-in enrichers (never the hot path): Anthropic count_tokens as a rate-limited calibration sampler (sends content off-box → opt-in only); host-native usage where it exists (Gemini AfterModel.usageMetadata.totalTokenCount).
  • Store — local, at data-root, aggregate counts only — never raw args/results. MVP = append-atomic NDJSON event log + derived rollups behind a TelemetryStore interface (SQLite/WAL is a drop-in upgrade). Rows keyed by connectorId, toolName, scope(call|tool_defs|model_turn|hook), surfaceKind(server|hook|command|skill|subagent), hostPlatform, sessionId, projectKey, projectDir, inputTokens, outputTokens, confidenceSource, isError, ts.
  • Surfaceagent-connector telemetry report [--by tool|session|project] [--since 7d] [--json] → ranked per-tool footprint, tool-def overhead line, input/output split, calls, tokens/call avg, per-session/project rollups, with a visible confidence label; plus telemetry export --format csv|json.
  • Privacy / opt-out — local-first, zero egress by default; granular kill switches AGENT_CONNECTOR_TELEMETRY=0 (global) + per-layer (measure / calibrate / upload); hashable aggregation keys; dashboards state numbers are estimates from the server's own I/O, not host-billed usage.

7. Public API (write once)

import { defineConnector } from "@ken-jo/agent-connector/sdk";

export default defineConnector({
  // package.json name/mcpName/bin/version are the default metadata source.
  // Set id or mcp.hostAlias only for legacy configs or multi-instance aliases.

  server: {                                  // declared ONCE, transport-polymorphic
    transport: "stdio",
    command: "npx",
    args: ["-y", "@acme/acme-db-mcp"],
    env: { ACME_DB_DSN: "${env:ACME_DB_DSN}" },   // universal ${env:VAR} / ${env:VAR:-default}
    tools: { include: ["*"] },
  },

  hooks: {                                   // normalized events; framework synthesizes per paradigm
    PreToolUse: {
      matcher: "acme_query|acme_write",
      async handler(evt) {
        if (evt.toolName === "acme_write") return { decision: "ask", reason: "Confirm write" };
        return { decision: "allow" };
      },
    },
  },

  memory: [{                                 // standing guidance → managed marker block in each
    content: "Use the acme-db MCP tools for schema questions; never hand-edit migrations.",
  }],                                        // host's memory file (AGENTS.md-first; CLAUDE.md/GEMINI.md exceptions)

  telemetry: { enabled: true, modelFamilyHint: "auto", measureToolDefs: true },

  platforms: {                               // per-platform escape hatch / overrides
    warp: { hooks: false },                  // mcp-only host: skip hooks gracefully
  },
  targets: "auto",                           // or ["claude-code","codex","cursor"]
});

7.1 Connector SDK — authoring, introspection, offline harness

The root export (@ken-jo/agent-connector) is unchanged. Two additive subpaths layer the developer-facing SDK on top of the abstraction model above.

@ken-jo/agent-connector/sdk — consolidated authoring surface. Re-exports defineConnector, ConnectorConfigError, the full define* typed-identity helper family, the introspection helpers, and the public types — one import site for all authoring.

The define* helpers are typed identity functions: they infer the right *Def type and return the definition unchanged; validation stays inside defineConnector. The family:

  • defineConnector(config) — existing; the root definition entry point.
  • defineStatusline({ render }) — existing; typed to StatuslineDef.
  • defineAction({ id, run }) — typed to ActionDef; see the action surface below.
  • defineCommand, defineSkill, defineSubagent, defineMemory, defineConfigPatch, defineNativeHook — each (def) => def, typed to its *Def.
  • defineHookevent-parameterized so the handler payload narrows to the concrete event type rather than the union:
    const onPre = defineHook("PreToolUse", {
      handler(evt) {
        // evt is typed as PreToolUseEvent, not the full event union
        return { decision: "deny", reason: "no" };
      },
    });
    The leading event string exists only to drive type inference; the def is returned unchanged.

Introspection (async — adapters load lazily):

  • capabilitiesOf(host): Promise<PlatformCapabilities | undefined> — a host's capability flags; undefined for an unrecognized id.
  • hostsSupporting(surface): Promise<PlatformId[]> — which registered hosts honor a surface. surface is a SurfaceName: server | hooks | commands | skills | subagents | memory | statusline | configPatch | nativeHooks | actions.
  • surfaceSupport(host, surface): Promise<boolean> — per-host / per-surface convenience predicate.
  • SURFACE_PREDICATES — the per-surface capability predicate map, exported for advanced use.

@ken-jo/agent-connector/sdk/test — offline harness. The two exports answer "does my handler / HUD actually work on host X?" without touching a real host.

  • explain(connector): Promise<ExplainRow[]> — the per-host × per-declared- surface support matrix. Each row is { host, surface, support: "native" | "skip-warn" | "disabled", reason }. Only surfaces the connector actually declares are included; configPatch / nativeHooks rows are scoped to the host that declares them; disabled = an explicit platforms[host].<surface> = false. (The server row is capability-based for v1 — registration may be host-managed on some IDEs; the row reason notes this.)

  • simulate(connector, { surface, host, event?, input }): Promise<{ honored, hostReply?, reason }> — runs the real adapter parse → handler → format chain offline and reports whether the host would actually honor the handler's decision. surface is "hooks" (requires event) or "statusline"; input is the host-shaped raw payload. It mirrors the runtime exactly: tolerant stdin, matcher filtering (a matcher-scoped handler that doesn't match is not run), and a verdict computed by parsing the actual host reply and judging the true (event, decision) contract — not substring guessing. This means it reports real host quirks honestly:

    • codex drops context on UserPromptSubmit (no stdout path) → honored: false.
    • a deny on Stop / SubagentStop is continuation/persistence, not a block → honored: true, reason says "continues … (persistence)".
    • a deny on SubagentStart / PostToolUseFailure degrades to a context note → honored: false.
    • a PermissionRequest ask is honored by the host's native dialog (no stdout) → honored: true.

In short: explain is the whole support matrix; simulate is the behavioral check that encodes each host's real honor / drop / degrade contract — the honest per-host reach, offline, before you touch a real host.

8. CLI

agent-connector detect                       # installed platforms + scope + capabilities + paradigm
agent-connector install [--scope user|project] [--targets a,b] [--connector path] [--dry-run]   # framework fallback / CI-debug; branded package/bin is the normal MCP lifecycle path
agent-connector uninstall [--targets ...] [--purge]   # full inverse — removes server + hook registrations; --purge also drops the connector's home state record (+ the shared launcher when none remain)
agent-connector upgrade [--channel stable|latest]   # bring all current: re-render host config + heal pointer + managed update guidance (alias: update, sync)
agent-connector doctor [--probe]             # per-platform health checks; --probe = live MCP handshake (initialize → ping → tools/list)
agent-connector status                       # light install-state per host (always exits 0)
agent-connector package [--connector path] [--format <fmt>|all]      # framework tooling: 10 host bundle formats; mcp-server-json|mcpb = OFFICIAL MCP standard artifacts (opt-in by name)
agent-connector telemetry report|export [...]
agent-connector usage report|export|leaderboard [...]   # host-native usage from agent CLI logs (read-only)
agent-connector leaderboard [--since] [--scope] [--connector]   # 🔌 mcp-self + 🖥️ host-scan-logs + 🛰️ host-native-live (never summed)
agent-connector hook <platform> <event> --connector <id>   # universal hook entrypoint (internal)
agent-connector serve --connector <id> -- <server cmd...>   # telemetry-wrapping MCP proxy (internal)

The user-facing install/doctor/uninstall path should normally be the developer's branded MCP package/bin, for example npx @acme/acme-db-mcp install or acme-db install. The agent-connector ... commands above are the framework surface: development fallback, CI/debug entrypoint, marketplace packaging driver, internal home-bin target, and connector-free usage telemetry utility.

Canonical CLI reference: the docs site /docs/cli and llms-full.txt §3 (kept current; drift-guarded by tests). This block is design context — when they disagree, the canonical reference wins.

Install per target: backupSettings() → render server config into native file → if hooks & paradigm≠mcp-only: synthesize entrypoint + write hook config + set exec bit → register in plugin registry where applicable → return change list. Everything idempotent, reversible, --dry-run-able.

9. MVP scope & phasing

  • Phase 0 — core spine (no platforms): contracts, defineConnector + validation, interpolation, registry/detection, spawn helpers, data-root/paths, doctor harness, CLI skeleton.
  • Phase 1 — MVP: 3 maximally-divergent, high-confidence platforms + telemetry core. Claude Code (JSON, mcpServers, richest hooks), Codex CLI (TOML, env split, trust gates), Cursor (JSON + ${env:} interpolation + hooks.json v1). This trio alone exercises JSON+TOML, three hook dialects, env-var detection, and the whole json-stdio path end-to-end. Ship telemetry core here (it's platform- independent by design).
  • Phase 2 — breadth on json-stdio + first host-native telemetry + first mcp-only: VS Code Copilot, Copilot CLI, Gemini (wire AfterModel host-native enricher), Warp.
  • Phase 3 — ts-plugin paradigm: OpenCode, Kilo, Hermes (the hard adapters).
  • Phase 4 — verification-gated long tail: JetBrains, Pi, OpenClaw, zed/antigravity/ kiro/qwen/kimi/omp.

10. Module layout

src/
  index.ts                 public API surface (defineConnector + types)
  core/
    types.ts               all shared contracts (ServerDef, events, config, scope, …)
    define-connector.ts    defineConnector() + validation + normalization
    interpolate.ts         ${env:VAR} / ${env:VAR:-default}
    managed-block.ts       marker-fenced managed blocks + memory ledger (the memory-surface engine)
    paths.ts               home data-root, project key/identity, per-OS dirs
    spawn.ts               Windows-safe build/parseNodeCommand, runtime resolution
    spawn-child.ts         Windows-safe child_process.spawn wrapper
    mcp-standard.ts        pinned official-MCP literals + guards (server.json / MCPB / wire)
    package-formats/       per-host marketplace bundle renderers (claude-family, gemini, …)
    logger.ts
  adapters/
    spi.ts                 Adapter interface (generalized HookAdapter + MCP render)
    registry.ts            ADAPTER_REGISTRY single source of truth (+ matrix test)
    detect.ts              install-time + runtime detection
    base.ts                BaseAdapter shared impl
    claude-code/  codex/  cursor/      (Phase 1)
  telemetry/
    types.ts  tokenizer.ts  measure.ts  store.ts  report.ts  proxy.ts
  runtime/
    index.ts  hook-entrypoint.ts  serve.ts  probe.ts
  cli/
    index.ts  app.ts  sdk.ts   (sdk.ts = createConnectorCli for branded CLIs)
    commands/{detect,install,uninstall,upgrade,package,doctor,status,telemetry,
              usage,leaderboard,hook,serve,usage-event}.ts