# 029 — Compact MCP tool surface with capability gateways (0.7.1)

Status: accepted, 2026-09-10.

## Problem

0.7.0 grew the catalog to 74 tools. The full tools/list serializes to roughly
236 KB (95 KB of name+schema payload alone). Remote MCP clients ingest every
tool definition at connection/scan time; the owner reported ChatGPT no longer
completing write/edit work right after the 0.7.0 upgrade and downgraded the
global install back to 0.6.3 (49 tools). Server-side evidence shows the write
path itself is healthy: the live audit log records 112 `write_file` ok and 91
`edit_file` ok in the previous 48 h from OAuth principals, and the full
HTTP+OAuth and STDIO fixtures pass write/edit round-trips. The remaining
0.7.0-specific change visible to a remote client is catalog size. No hard
per-client tool limit is claimed; compact mode reduces catalog/schema load and
avoids requiring clients to ingest every detailed operation schema at
connection time.

## Decision

Two tool surfaces over ONE unchanged capability set:

- **full** — the existing 74-tool catalog, byte-for-byte the same names,
  order, schemas and behavior. Default for STDIO (Codex, Claude Code, Cursor).
- **compact** — 19 tools: `project_overview`, `juju_discover`, and 17 domain
  gateways (`juju_read`, `juju_write`, `juju_exec`, `juju_git_read`,
  `juju_git_write`, `juju_desktop_view`, `juju_desktop_control`,
  `juju_assist_read`, `juju_assist_change`, `juju_media`, `juju_browser`,
  `juju_game`, `juju_workflow_read`, `juju_workflow_write`,
  `juju_workflow_run`, `juju_schedule`, `juju_web`). Default for HTTP.
  A gateway call is `{ workspaceId, workspaceEpoch, operation, args }` where
  `operation` is enum-bound to that gateway's fixed allowlist; every
  full-catalog tool except `project_overview` is reachable through exactly one
  gateway. `juju_discover` searches operations and returns one operation's
  full input JSON Schema plus a deterministic `schemaHash`.

Selection: `toolSurface` in the global config overrides the transport default;
`--tools compact|full` on `juju start`/`juju stdio` overrides both for one
run. The setting changes tool exposure only — never permissions.

## Security invariants (unchanged, now single-pathed)

`registerTool` and the gateways route through one function,
`invokeToolDefinition` (src/tools/context.ts): live principal →
WORKSPACE_ACCESS_REQUIRED on empty workspace ACL → the TARGET tool's
`requiredScope` → workspaceId/epoch context → request byte budget → the
target's ORIGINAL strict Zod input schema → the target handler (which runs its
own `policyGate`/local approvals, idempotency and journaling) → envelope →
audit. Consequences, all covered by tests:

- A read-only token calling `juju_write(write_file)` fails `FORBIDDEN` with
  the target's scope in `detail`, exactly like the direct call.
- In `inspect` mode an effectful gateway call produces an approval bound to
  the TARGET operation (`tool: write_file`), never a blanket gateway approval.
- `args` may not carry `workspaceId`/`workspaceEpoch` (rejected
  `INVALID_INPUT`); the gateway injects the top-level context itself.
- Target error codes (`FILE_CHANGED`, `AMBIGUOUS_EDIT`, `SECRET_PATH_DENIED`,
  `APPROVAL_REQUIRED`, …) pass through unchanged via `fromErrorInfo`.
- Image/audio MCP content blocks pass through unchanged.
- Gateways cannot target other gateways, `project_overview`, owner IPC
  controls or anything outside the fixed allowlists (construction-time
  invariant + tests). Each gateway's own `requiredScope` is computed as the
  minimum of its targets, so it can never be broader than the catalog.
- Both the gateway call and the target invocation are audited.

## Evidence

- Serialized tools/list-equivalent payload (name+title+description+annotations+
  input+output schemas): 74 tools = 237 755 bytes; compact 19 = 46 834 (−80.3%);
  measured live tools/list: 236 518 → 46 707 bytes. Recorded by the
  `[juju] mcp tool surface` startup diagnostic and `schemas/tools.compact.json`.
- Suites: `tests/unit/compactSurface.test.ts`,
  `tests/security/compactGateway.test.ts`,
  `tests/integration/compactHttp.test.ts`,
  `tests/integration/compactStdio.test.ts`,
  `tests/integration/compactCapabilities.test.ts`, packaging PACK-12; the 24
  direct-contract suites pin `toolSurface: 'full'` explicitly.
- Live ChatGPT re-scan against the compact surface is a MANUAL gate
  (MANUAL_ACCEPTANCE.md); no client-side limit figure is asserted.

## Addendum (0.7.2): hybrid surface

The owner's live ChatGPT connector displayed exactly 49 tools against a
74-tool catalog — the first 49 in catalog order — consistent with a
client-side tool-count cap near ~50 (still not asserted as a universal
constant). For clients in that range that prefer direct tools, 0.7.2 adds the
opt-in `hybrid` surface: 49 tools, compact coverage core first (truncation-
safe), then 30 direct coding tools (`HYBRID_DIRECT_OPERATIONS`). Selection
via config/CLI as before; defaults unchanged; every path keeps the shared
invocation pipeline.
