0075 — External interfaces: a first-class protoagent CLI + tightened MCP / API / model onboarding
- Status: Accepted
- Date: 2026-07-05
- Implementation: D1 CLI (#1816), D3 MCP profiles + HITL-hang fix (#1817), D5
protoagent model(#1821), D4 goal-API dedupe (#1822) · real/v1usage (#1825), and D2 the sharedops/layer —GET /api/mcp/exposed+ resolver extraction (#1824), theops/package +knowledge.ingest(#1826),plugins.install_and_activate(#1827),config+fleetops (#1828), theGET /api/operationscatalog +protoagent operations/config get·setCLI (#1830), andprotoagent knowledge ingest(#1831) — are shipped. Remaining: thesafe-operatorMCP profile (needs the admin ops exposed as MCP tools + the ADR 0071 consent gate) and the D5 HuggingFace local-app registration (external PR). - Builds on: ADR 0033 (ACP runtime + operator MCP server), 0027 (plugin CLI + installer), 0041 (workspace CLI + tiered stores), 0042 (fleet CLI + supervisor), 0047 (settings cascade +
config explain), 0019 (managed / consumed MCP), 0066 (federation token + the/apioperator ceiling), 0071 (plugin trust-and-consent). - Related: protoCLI
proto— the A2A client + ACP server (ADR 0024/0025); #1504.
Context
protoAgent has three external interfaces — a CLI, an operator MCP server, and an HTTP API (/api + OpenAI-compat /v1 + A2A /a2a). Each grew on its own, and an audit (2026-07-05) found they are two disjoint shared spines, not one operation model:
- Agent-tool spine — the native tool-loop and the operator MCP both re-project
tools/lg_tools.py::get_all_tools(). One source of truth; the MCP is a genuine re-projection, not a reimplementation. ✅ - Instance-admin spine — the CLI and the REST API both call the
graph/**cores (installer.install,fleet.supervisor,config_explain, …). Shared per pair. ✅ - The two spines barely intersect. Admin operations (plugin install, fleet up, config edit, MCP-server management) are not agent tools, so they can never appear on the MCP; most agent tools have no REST twin. Only
knowledge_ingestspans both — and it duplicates its extract/transcribe/add glue in the tool and the route.
Three concrete symptoms:
python -m server …is the only admin front door, and it's undiscoverable. Theplugin/workspace/fleet/skills/configsubcommands are hiddenif sys.argv[1] == "plugin"branches inserver/__init__.py::_main()— they don't even appear in--help. There is no lifecycle verb (up/down/status), nosetup, no installable command name. It reads as an internal module invocation, not a product.- "Operate the instance over MCP" is only half-true.
server/operator_mcp.pyexposes the conversational toolset (memory / notes / tasks / goals / skills /delegate_to) but not administration — you cannot install a plugin, drive the fleet, or edit config over MCP. (It also has a real bug:ask_human/request_user_inputare HITLinterrupttools included in the"*"set; called over a foreign stdio MCP they hang the turn.) - The REST surface has drift —
goalsvsgoal, and one memory store split across/api/memory+/api/knowledge+/api/memory/injections(three modules, two prefixes); plus REST-only orchestration glue (install-and-activate, ingest) hand-written on top of the shared cores, so a CLI install can't hot-reload a live server.
The one part that is already clean: authentication. a2a_impl/auth.py::A2AAuthMiddleware is a single default-deny gate covering /api, /v1, and /a2a identically (Bearer / X-API-Key / federation-token / SSE query-token; open mode when unset). This ADR reuses it unchanged.
Separately, onboarding a local LLM (Ollama, HuggingFace TGI, LM Studio, vLLM) to power protoAgent is a pure config move today — graph/llm.py::create_llm builds a plain ChatOpenAI(base_url=config.api_base, model=config.model_name), so the LiteLLM gateway is the default, not a lock-in — but there is no discovery, no model command, and no "use in app" handoff, so it isn't fast.
proto (protoCLI) already owns two legs of the agent relationship — a faithful A2A client (packages/cli/src/a2a-client/, mirroring evals/client.py + the :7870-7879 port-scan) and an ACP server (proto --acp, so protoAgent's delegate_to acp spawns it). It has zero runtime lifecycle — it assumes a protoAgent is already running. So the missing leg is unambiguously protoAgent's own to own.
Decision
Treat the CLI, MCP, and API as one operation model with three faithful projections, and give protoAgent a first-class terminal identity. Five decisions:
- D1 — A first-class
protoagentCLI: the admin + lifecycle control plane. Retirepython -m server <sub>as the user-facing entrypoint in favor of an installableprotoagentcommand with a real subcommand tree and--help. Keepprotoas the interactive A2A chat client; the two meet at the wire (A2A / ACP), never in one binary. Locked: the command is namedprotoagent. - D2 — A shared
ops/layer. Promote the currently REST-only orchestration (install-and-activate, ingest, fleet up, config set) into one function per operation that carries the full orchestration, consumed by all three surfaces. "One operation, three projections" — the CLI verb, the MCP tool, and the REST endpoint call the same op. - D3 — Extend the operator MCP to the FULL admin surface, via curated profiles + consent. The admin ops from D2 become MCP-callable, so a foreign brain (Claude Desktop, Cursor) can
plugin install,fleet up, assign work, etc. Gated by (a) curated profiles (read-only/safe-operator/full) instead of all-or-name-by-name, (b) ADR 0071 trust-and-consent for the admin tier, and (c) the ADR 0066/api-ceiling posture (admin ops are the very set that ceiling protects). Fix theask_human/request_user_inputhang (exclude from"*", likeexecute_code). Locked: full admin over MCP. - D4 — Tighten the HTTP API now, breaking changes accepted. Dedupe the split namespaces (
goal[s],memory/knowledge/injections), fill in real/v1usage(stubbed to zeros today), and publish a machine-readable operation catalog the three surfaces derive from. Ship one release of deprecation aliases, then delete. Locked: do it now, complete it. - D5 — Fast local-LLM onboarding + "use in app".
protoagent setupdiscovers running local providers; aprotoagent modelverb group manages the model; and protoAgent registers as a HuggingFace "Use this model" local-app so a HF model card hands a model straight to it. Revised after research (2026-07-05): for an agent runtime the "use in app" contract is a CLI snippet via a PR tohuggingface.jslocal-apps.ts, NOT aprotoagent://deep-link — protoAgent maps onto the existinghermes-agent/openclaw/pipattern (which are the Hermes/OpenClaw competitors, already registered on the same cards). A deep-link is only for a GUI loader (optional, secondary). Locked: ship bothuv tool/pipxand a zero-Python frozen-binarycurl | shinstall path.
Design details
A. The protoagent CLI
A console_scripts entry (protoagent = "server.cli:main") over a real argparse/typer subparser tree. The existing dispatchers are re-parented, not rewritten:
protoagent setup # interactive provider/model/key wizard (new; mirrors proto setup)
protoagent up | down | status # lifecycle: start/stop/inspect the default instance (new)
protoagent serve [--port] # run in the foreground (= today's `python -m server`)
protoagent plugin … # ← graph/plugins/cli.py (exists — re-parent)
protoagent workspace … # ← graph/workspaces/cli.py (exists — re-parent)
protoagent fleet … # ← graph/fleet/cli.py (exists — re-parent)
protoagent skills … # ← graph/skills/cli.py (exists — re-parent)
protoagent config … # ← graph/config_explain.py (exists — re-parent)
protoagent knowledge ingest|search # over the store / the D2 ingest op (new, thin)
protoagent model … # local-LLM onboarding (see E) (new)
protoagent mcp serve|profiles # run/inspect the operator MCP (see C) (new, thin)up/down/status reuse the fleet supervisor's process model (detached spawn + boot-watch + liveness) applied to the default instance — no new supervision primitive. Chat is deliberately absent: it's proto's job. python -m server keeps working as the internal module form (the frozen sidecar re-invokes it); protoagent serve is the friendly alias.
B. The shared ops/ layer
A new ops/ package: one module per domain, each a function that takes plain args + a context and returns a plain result, wrapping the graph/** core plus the orchestration that is REST-only today. e.g. ops.plugins.install_and_activate(url, ref, *, activate: bool) calls installer.install and the auto-enable + hot-reload dance (plugin_routes.py L202-260) that a CLI install currently can't do. The three surfaces become thin adapters:
- CLI adapter → parse argv → call the op → render text/JSON.
- REST route → validate body → call the op → JSON.
- MCP tool → the op wrapped by
to_fastmcp, admitted by profile (C).
knowledge_ingest (the one cross-spine op) collapses to a single ops.knowledge.ingest shared by the agent tool and the route, ending the double-glue.
C. Operator MCP: admin profiles + consent
Extend operator_mcp.operator_tools() to admit the D2 admin ops, selected by a named profile rather than a raw allowlist:
read-only— status / list / explain / search (no mutation).safe-operator— read-only + curation + non-destructive ops; no install / fleet control / config-rewrite.full— everything, incl.plugin install,fleet up/down,config set, work assignment. Requires an explicit consent ack (ADR 0071) recorded per client, and honors the ADR 0066/apiceiling (a federation-token peer never reachesfull).
The "*" set excludes the HITL interrupt tools (ask_human, request_user_input) alongside execute_code. Add GET /api/mcp/exposed so the exposed set is introspectable (today only the consumed MCP is listed).
D. HTTP API tightening (breaking, aliased one release)
- Namespaces: collapse
goal→goals(member =/api/goals/{session_id}); unify the memory store under/api/memory/*withknowledgechunks andinjectionsas sub-resources (/api/memory/chunks,/api/memory/injections). Keep the old paths as 307-aliases with aDeprecationheader for one minor release, then delete. /v1: populate realusage(prompt/completion/total) from the turn's token accounting (the same numberscost-v1reports over A2A) so OpenAI-SDK clients + cost tooling work.- Operation catalog: a generated
GET /api/operations(andprotoagent config operations) enumerating every D2 op with its params + which surfaces expose it — the "one operation, three projections" made introspectable, and the source the CLI help + MCP tool list derive from.
E. Local-LLM onboarding + "use in app"
protoAgent already accepts any OpenAI-compatible api_base, so onboarding is discovery + a one-liner, not new model plumbing:
- Discovery —
protoagent setup(andprotoagent model discover) probe well-known local endpoints — Ollama (http://localhost:11434/v1,/api/tags), LM Studio (:1234/v1), vLLM / HF TGI (/v1/models) — and list the models each serves (mirrors proto'smodelDiscovery). protoagent modelverbs —list(configured + discovered),use(the load-bearing one:protoagent model use --base-url http://127.0.0.1:8080/v1 --model <id>setsapi_base+model_name+ a placeholder key via the config op, non-interactively, no gateway required),add/remove(named endpoints),pull <name>(shell out toollama pullwhen present). Must tolerate the HF quant-tag placeholder:(default to a sensible quant).- "Use in app" handoff — a HuggingFace local-app registration, not a deep-link (research 2026-07-05). protoAgent is an OpenAI-compatible agent runtime, so it registers exactly like
hermes-agent/openclaw/pi: a ~20-line PR topackages/tasks/src/local-apps.tsinhuggingface/huggingface.js, gated onisToolCallingLocalAgentModel(GGUF/MLX +conversational- a tools chat-template), emitting a 3-step snippet (not a
deeplink): (1)getLocalServerStepstarts llama.cpp/MLX on:8080/v1, (2)protoagent model use --base-url http://127.0.0.1:8080/v1 --model ${modelId}, (3)protoagentto launch. Once merged it shows on every compatible model card's "Use this model" dropdown. Ollama offers no such registry — it's OpenAI-API-only (:11434/v1), reached by the sameprotoagent model use. Aprotoagent://deep-link is only worth adding if we ship a GUI to foreground (optional, secondary). The prereqs the snippet needs are exactly this ADR's PR1 (installable command) + D5 (model useone-liner).
- a tools chat-template), emitting a 3-step snippet (not a
- Leverage the HF quant reputation, not social clout. Two compounding surfaces: (A) the registry PR puts protoAgent on other people's compatible cards; (B) — works today, no HF dependency — the team's own published quant model cards carry a "Run with protoAgent" snippet and
conversational+ tools-template metadata, so they auto-surface protoAgent the moment the PR merges. Converts at the moment of intent (a user on the card about to run the model).
F. Install / distribution
Both, binary-first for the headline:
- Zero-Python — the existing PyInstaller frozen binary (the desktop build) shipped as
protoagentbehindcurl -fsSL https://…/install.sh | sh(mirroring proto'sinstall.sh: detect arch/OS, fetch, put on PATH, runprotoagent setup). Also a Homebrew tap. - Python present —
uv tool install protolabs-agent/pipx install protolabs-agent(the PyPI name;protoagentis similarity-blocked) → theprotoagentconsole_scriptsentry on PATH.
G. Auth
Unchanged. The CLI talks to a local instance with the resolved Bearer (from config/env); the MCP admin profiles ride the same default-deny middleware and the ADR 0066 ceiling. No new credential model.
Rollout
- PR1 — the
protoagentCLI (D1): entrypoint + subparser tree +--help, re-parent the five existing dispatchers, addup/down/status/serve/setup. Low risk (≈80% repackaging); immediately kills the "boringpython -m server" front door. - PR2 — the
ops/layer + MCP admin profiles (D2 + D3): extract the shared ops, wire the three adapters, add profiles + theask_humanexclusion +GET /api/mcp/exposed. - PR3 — API tightening (D4): namespace dedupe (aliased),
/v1usage,/api/operationscatalog. - PR4 — model onboarding (D5): discovery +
protoagent model+ the install scripts; theprotoagent://deep-link + "use in app" as a fast-follow once its payload contract lands.
Install/distribution (F) rides PR1 (binary + uv tool) and PR4 (the install.sh polish).
Consequences
- One terminal identity.
protoagent up && protoagent plugin install … && protoagent fleet upreplaces a grab-bag ofpython -m server,uv, and docker — and it's discoverable via--help. - "Operate over MCP" becomes true — a foreign brain can administer a protoAgent within an explicit, consented profile, not just chat with it.
- The surfaces stop drifting — a new operation is one
ops/function that appears on all three surfaces by construction, listed in one catalog. - Breaking API changes land once (aliased a release) instead of accreting a third idiom.
- Local models are a click — the Ollama/HF ecosystems can hand a model to protoAgent.
- Cost: a real
ops/seam + adapters is more indirection than three ad-hoc surfaces; the payoff is the anti-drift guarantee.
Open questions
- HF local-app PR — acceptance + gate choice (D5). (Resolved mechanism, open on execution.) The registration is a PR to
huggingface.jslocal-apps.ts(review ≈ 1–2 wk, no formal bar, GGUF/MLX is the ticket). Open: use the narrowisToolCallingLocalAgentModelgate (the honest agent-runtime precedent — reviewers will steer here) vs. the broadisLlamaCppGgufModel(all ~45K GGUF cards, but over-reaching draws a change request)? And do we also ship a GUIprotoagent://deep-link, or snippet-only like the other three agent runtimes (snippet-only)? - CLI ↔ running-server vs on-disk ops. Some ops act on disk (plugin install), some need a live server (fleet status, hot config). Does the CLL auto-boot a transient server for live-only ops, or require
protoagent upfirst? (Lean: disk ops work headless; live ops error with "runprotoagent up" unless--ensure-up.) fullMCP profile blast radius. A consented foreign brain doingfleet up/ work assignment is powerful; do we cap it (rate/scope) beyond the consent ack, or trust the ADR 0071 posture?- Homebrew/npm namespace.
protoagenton Homebrew + a possible@protolabsai/protoagentthin npm shim (soprotousers discover it) — worth it, or binary +uvonly?
Alternatives considered
- Fold runtime management into
proto(the first instinct) — rejected:protois a Qwen-fork terminal coding agent with its own identity; bolting a Python-runtime control plane onto it conflates two products and two audiences ("trips ourselves up"). The clean line is:prototalks-to / is-driven-by protoAgent over the wire;protoagentruns and manages the runtime. - Keep
python -m server <sub>— rejected: undiscoverable, un-installable, reads as an internal, and blocks the "one command" onboarding story. - One giant unified registry replacing REST + CLI + MCP — rejected as over-reach: the two existing spines (tool registry,
graph/**cores) are already sound; the fix is a thinops/seam over the admin spine + faithful projections, not a rewrite. - A LiteLLM-gateway-only model story — rejected for local onboarding: the gateway stays the default, but requiring it for "point at my Ollama" is friction the direct
api_basepath already avoids.