From bcdc9a9a881bd1d4d4e48d5bc1d50394c50aa022 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:56:19 -0500 Subject: [PATCH 01/19] docs: define MCP command architecture Define the target peer-adapter architecture, typed command contracts, explicit inventory, access policy, transport separation, testing expectations, and incremental migration from the experimental version-only server. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 7 + design/mcp.md | 914 ++++++++++++++++++++++++++++++++++++ docs/guides/agentic-sdlc.md | 1 + docs/reference/mcp.md | 5 + 4 files changed, 927 insertions(+) create mode 100644 design/mcp.md diff --git a/AGENTS.md b/AGENTS.md index 2daaa2ea59..704f45699c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,13 @@ naming, private command phases, nested command groups, registration ownership, mirrored tests, and the rationale for making the CLI structure predictable for both humans and coding agents. +## Adding or Updating MCP Commands + +Before adding or changing MCP tools for Specify commands, read +[Specify MCP Command Architecture](design/mcp.md). It defines the shared +operation boundary, peer CLI/MCP adapters, typed contracts, explicit inventory, +access policy, transport separation, and mirrored tests. + ## Adding or Updating Agent Integrations Before adding or changing AI agent integrations, read diff --git a/design/mcp.md b/design/mcp.md new file mode 100644 index 0000000000..ffeccdf7a5 --- /dev/null +++ b/design/mcp.md @@ -0,0 +1,914 @@ +# Specify MCP Command Architecture + +This document defines the target architecture for exposing Specify operations +through the Model Context Protocol (MCP). It is the MCP counterpart to +[Specify CLI Command Architecture](cli.md): command paths, ownership, +registration, contracts, tests, and migration should be predictable from the +surface being changed. + +The current `src/specify_cli/mcp_server/` implementation is intentionally +experimental and transitional. It exposes only `version` through generic +list, describe, and run tools, invokes the CLI in a child process, and parses +the CLI JSON result. That is a bounded first implementation, not the target +architecture described here. + +## Design goals + +The MCP structure should make the answer to "where does this tool's behavior +live?" as predictable as the equivalent CLI question. + +The design optimizes for: + +- **One logical operation:** a CLI leaf and its MCP tool are two adapters for + the same application behavior. +- **Adapter parity:** CLI and MCP inputs, results, warnings, errors, and side + effects remain semantically equivalent. +- **Typed discovery:** each available MCP operation has a command-specific + input and output schema. +- **Explicit exposure:** every CLI leaf has a reviewable MCP inventory + disposition; tools are never exposed through filesystem discovery. +- **Small working context:** changing one operation should normally require + only its domain, CLI adapter, MCP adapter, and mirrored tests. +- **Policy visibility:** project writes, execution, network access, trust + decisions, and self-modification are declared and enforced. +- **Transport independence:** stdio and future Streamable HTTP hosting do not + change command behavior. +- **Incremental migration:** commands move to the shared model without a flag + day or compatibility break. + +## Non-goals + +This design does not: + +- Make MCP a wrapper around the human CLI. +- Require every CLI leaf to be remotely invokable regardless of risk or + readiness. +- Turn existing human output into an MCP or JSON contract. +- Make `--json`, `--non-interactive`, or an MCP invocation imply `--force`, + trust, destructive consent, or network permission. +- Define a public Python API for third-party callers. The supported external + surfaces remain the CLI, MCP contracts, integrations, and workflow steps. +- Replace command-owned application behavior with one universal command + engine or central service locator. +- Require an otherwise simple operation to be split into extra modules only + for visual symmetry. + +## One logical operation, two adapters + +For every MCP-eligible CLI leaf, the architecture has three conceptual +layers: + +```text +CLI arguments/options ─┐ + ├─> shared operation request -> application behavior +MCP tool input JSON ───┘ -> operation outcome + +operation outcome ─────┬─> CLI human or JSON rendering + └─> MCP structured content or tool error +``` + +The CLI and MCP adapters are peers: + +- The CLI adapter owns Typer/Click declarations, terminal interaction, human + rendering, exit codes, and CLI JSON serialization. +- The MCP adapter owns tool metadata, MCP input/output schemas, protocol + result conversion, and MCP annotations. +- The shared operation owns semantic validation, orchestration, side effects, + typed results, warnings, and structured domain errors. + +Neither adapter calls the other. In particular, the target MCP adapter must +not invoke Typer handlers, start `specify` as a child process, scrape Rich +output, or parse CLI stderr. + +The shared operation is the behavioral source of truth. Adapters may differ +in presentation, but they must not differ in what the operation means. + +## Ownership boundaries + +| Concern | Owner | +| --- | --- | +| Semantic request and result models | The relevant command/domain hierarchy | +| Semantic validation and orchestration | The shared operation/application module | +| Domain errors and warning codes | The relevant command/domain hierarchy | +| CLI arguments, prompts, text, JSON streams, and exit codes | `command_.py` | +| MCP tool name, description, annotations, and protocol conversion | `mcp_.py` | +| Per-group MCP registration and command inventory | The hierarchy's `_mcp.py` or small package registration module | +| Cross-command invocation context and access-policy primitives | Shared MCP/application infrastructure | +| MCP server lifecycle and transport hosting | `specify_cli/mcp_server/` | +| Authentication and connection concerns for future HTTP hosting | The HTTP transport layer | + +Command-specific contracts do not belong in a central MCP catalog. Shared MCP +infrastructure may define primitives such as `InvocationContext`, +`OperationWarning`, `OperationError`, access policy, cancellation, and output +budgets. It must not accumulate command-specific request models, result +models, validation, or orchestration. + +## Operation identity and MCP tool design + +Each logical operation has three related identities: + +| Surface | Example | +| --- | --- | +| Logical operation ID | `artifact.list` | +| CLI path | `specify artifact list` | +| MCP tool name | `specify_artifact_list` | + +Operation IDs use dot-separated CLI path segments without the leading +`specify`. MCP tool names use the same segments with underscores and a +`specify_` prefix. Hyphens in CLI segments become underscores: + +```text +specify extension set-priority +operation: extension.set-priority +tool: specify_extension_set_priority +``` + +An explicit override is allowed only to satisfy a protocol restriction or +resolve a demonstrated collision. The inventory must record the override and +the reason. + +### First-class tools, not a generic execution facade + +The target surface exposes each *eligible* CLI leaf as a first-class MCP tool. +A generic `specify_run_command` facade is not the target. + +The current MCP SDK creates one JSON input schema per registered tool from the +tool's typed callable. First-class tools therefore preserve: + +- Per-command schemas and descriptions. +- MCP client discovery and argument validation. +- Command-specific output schemas and annotations. +- Reviewable registration and policy metadata. +- A direct mapping back to the CLI leaf and owning source files. + +A generic facade would instead reduce the protocol-visible input to a command +string plus an opaque or oversized union of arguments. That weakens schema +validation, discoverability, policy review, and compatibility analysis. + +At the time this document was written, the CLI had 90 leaf commands. +`specify mcp` is the transport host and is permanently excluded from recursive +exposure, leaving 89 leaf operations that require an explicit inventory +disposition. This count is large enough that registration must be organized +per command hierarchy, but not a reason to erase command-specific contracts +behind a generic tool. Tests should derive the current count rather than +hard-code 90. + +### Current transitional inventory + +The point-in-time CLI leaf inventory used to establish this design is: + +```text +root: init, check, version, mcp +self: check, upgrade +extension: add, disable, enable, info, list, remove, search, set-priority, + update +extension.catalog: add, list, remove +integration: install, uninstall, switch, upgrade, list, status, use, search, + info, scaffold +integration.catalog: add, list, remove +event: run +preset: list, add, remove, update, search, resolve, info, set-priority, enable, + disable +preset.catalog: add, list, remove +artifact: list, info, lookup +bundle: search, info, list, install, add, update, remove, validate, build, init +bundle.catalog: add, list, remove +workflow: run, resume, status, list, add, remove, update, enable, disable, + search, info, resolve +workflow.catalog: add, list, remove +workflow.step: list, add, remove, search, info +workflow.step.catalog: add, list, remove +workflow.overlay: add, set-priority, enable, disable, remove, list +``` + +In the current experimental server: + +- `version` is available only through the transitional generic tools and maps + to the target first-class tool `specify_version`. +- `mcp` is excluded because it is the transport host. +- The other 88 leaves are unavailable through MCP. Their initial target + disposition is `deferred` until their shared operation, typed contract, and + access-policy behavior satisfy this design. + +This list records the migration baseline, not a second registration source. +Once implemented, the hierarchy-owned inventory and its parity tests are +authoritative. + +Metadata-only inventory or describe tools may remain for compatibility or +diagnostics. They do not replace first-class operation tools, and a generic +run tool should be deprecated after migrated tools cover its supported +operations. + +## Naming and file layout + +MCP adapters mirror the CLI command path and live beside the owning command +and domain code: + +```text +src/specify_cli/extensions/ +├── __init__.py +├── _commands.py +├── _mcp.py +├── command_add.py +├── mcp_add.py +├── command_list.py +├── mcp_list.py +└── catalog/ + ├── __init__.py + ├── _mcp.py + ├── command_add.py + ├── mcp_add.py + ├── command_list.py + └── mcp_list.py +``` + +The conventions are: + +- `command_.py` is the sole CLI adapter for a real leaf command. +- `mcp_.py` is the sole MCP adapter for the same logical operation. +- `_mcp.py` owns explicit MCP registration and inventory for a command group. +- Nested directories continue to correspond to real CLI namespaces or bounded + subdomains, following [the CLI design](cli.md#nested-command-groups). +- The shared operation lives in the closest Typer-free domain module when one + already exists. +- If a dedicated application entry point is needed, use + `_operation_.py`. +- Tests mirror these names under `tests/specify_cli/`. + +Do not create a top-level MCP mirror of the entire CLI tree under +`mcp_server/commands/`. That would separate command contracts from their +owning domains and make unrelated command hierarchies depend on a central +package. + +Do not add `_mcp.py` merely for symmetry. A package with one small tool may +register it through an existing focused registration module. Create `_mcp.py` +when the hierarchy needs an explicit list of several tools, shared adapter +helpers, or inventory dispositions. + +## Simple and complex operations + +The same cohesion rules used for command-private phases apply below both +adapters. + +### Simple operation + +A simple operation may use an existing domain module or one focused operation +module: + +```text +command_version.py +mcp_version.py +_version.py +``` + +Both adapters map to the typed operation in `_version.py`. No extra phase +module is required. + +### Complex operation + +When a shared operation has cohesive phases with distinct invariants or +failure behavior, use: + +```text +_operation_.py +_operation__.py +``` + +For example: + +```text +command_init.py +mcp_init.py +_operation_init.py +_operation_init_validation.py +_operation_init_plan.py +_operation_init_apply.py +_operation_init_finalize.py +``` + +`_operation_init.py` remains the application entry point. Phase modules do not +register CLI commands or MCP tools. + +Adapter-only phases retain adapter-specific names: + +```text +_command__.py +_mcp__.py +``` + +Use them only when the phase genuinely belongs to presentation or protocol +adaptation. If both adapters need the phase, it belongs below them as an +operation or domain phase. + +Do not split a linear operation because it crossed an arbitrary line count. +Split when a phase has distinct invariants, rollback behavior, inputs, +outputs, or tests. + +## Registration and explicit inventory + +Registration remains explicit at each command-group boundary. + +For a group such as `artifact`: + +1. `artifacts/_mcp.py` lists every `artifact.*` CLI leaf. +2. Each leaf has exactly one inventory record. +3. Available records import and register their `mcp_.py` adapter. +4. Deferred, policy-disabled, and excluded records state a reason. +5. The root MCP composition module calls `artifacts._mcp.register(...)`. + +The root MCP server may aggregate hierarchy registration functions, but it +must not own their command-specific schemas or dispatch behavior. Registration +imports are explicit and consistently ordered. Filesystem scanning, +`command_*.py` introspection, and decorator side effects are not substitutes +for an inventory. + +A conceptual inventory record contains: + +```text +operation_id +cli_path +mcp_tool_name +contract_version +availability +availability_reason +side_effect_class +network_access +project_scope +default_timeout +``` + +The static disposition values are: + +- `available`: implemented and registered as a first-class tool. +- `deferred`: the CLI leaf exists, but shared operation extraction or a typed + MCP contract is incomplete. +- `excluded`: the command is intentionally not an MCP operation. + +An available tool may have an effective runtime state of `policy-disabled`. +It remains discoverable with its typed schema and annotations, but invocation +returns a structured `policy_denied` error. This keeps discovery stable across +policy profiles and prevents a host configuration change from changing tool +identity. + +Every CLI leaf must appear exactly once. The inventory parity test fails for a +missing leaf, duplicate logical operation, duplicate tool name, stale CLI +path, or unexplained exclusion. + +### Permanent and conditional exclusions + +`specify mcp` is permanently excluded because it hosts the MCP transport; an +MCP tool that starts another MCP server would be recursive infrastructure, not +an application operation. + +Other commands are not excluded merely because they mutate state. They are +classified and gated. For example: + +- `self.upgrade` is self-modifying and should be unavailable under the + default local policy. Exposure requires an explicit administrative policy + and a command contract that preserves upgrade safeguards. +- `event.run`, `workflow.run`, and `workflow.resume` are execution operations + and require execution policy. +- `init`, add/remove/update commands, and configuration changes are + project-write operations, with any stronger capabilities declared + separately. +- A command that still prompts, writes directly through its Typer handler, or + lacks a typed result remains `deferred` until those concerns move into a + shared operation. + +Exclusion and deferral are reviewable architecture decisions, not silent +omissions. + +## Command contracts + +### Typed requests + +The command/domain hierarchy owns a typed request model for semantic inputs. +The model: + +- Uses domain names rather than CLI flag spellings. +- Distinguishes omitted values from explicit false or empty values. +- Rejects unknown fields. +- Represents paths, enums, identifiers, and bounded collections explicitly. +- Contains explicit consent fields such as `force` or + `trust_extension_urls` only when the operation supports them. + +The CLI adapter maps parsed arguments and options into the request. The MCP +adapter exposes a command-specific JSON schema and maps validated tool input +into the same request. + +Typer usage errors remain CLI concerns. Semantic errors such as an unknown +integration, invalid project state, or incompatible options belong to the +operation so both adapters report the same failure. + +### Typed results and warnings + +The operation returns a typed outcome containing: + +- The command-specific result. +- Zero or more structured warnings. +- Execution metadata needed by adapters, such as changed paths or whether a + transaction committed. + +Warnings have a stable `code`, human-readable `message`, and optional typed +details. A warning is not printed inside the operation. The CLI adapter +renders it to the appropriate human or JSON channel; the MCP adapter includes +it in the command-specific structured result. + +Output types remain command-owned. There is no mandatory universal +`{"ok": true, "result": ...}` envelope. A shared outcome type is an internal +application mechanism, not a reason to replace established machine contracts. + +### Structured errors + +Expected failures use a typed operation error with: + +```text +code +message +details +retryable +exit_code +``` + +The operation hierarchy owns the error code and details schema. The CLI +adapter maps it to human output or the command's JSON failure contract and +then uses the declared exit code. The MCP adapter maps it to an MCP tool error +with structured content. Neither adapter exposes a traceback, secret, raw +subprocess output, or success-shaped fallback. + +Unexpected exceptions are normalized at the adapter boundary to a sanitized +`internal_error`, logged only through the transport-appropriate diagnostic +channel. + +## Machine contracts and version metadata + +Existing CLI JSON contracts are compatibility constraints. Extracting a +shared operation must preserve field names, value semantics, stdout/stderr +purity, and error behavior unless a separately reviewed contract change says +otherwise. + +Each logical operation declares a `contract_version` in its inventory and MCP +tool metadata. The version identifies the request/result/warning/error +contract, not the MCP transport version or CLI package version. + +Contract version metadata must not be injected into an established result +whose schema does not already contain it. For example, the current +`specify version --json` result intentionally returns `cli_version`, +`runtime`, `system`, and `features` directly. Its MCP tool should preserve +that result shape while exposing the contract version through tool or +inventory metadata. + +Rules for contract evolution: + +- Backward-compatible optional fields and new warning codes may retain the + current major contract version. +- Removing, renaming, or changing the meaning of an input, output, warning, + or error requires a new major contract version and a migration plan. +- CLI and MCP adapters for the same operation advertise the same contract + version. +- An adapter-only presentation change does not change the operation contract + version. +- Tests lock established JSON shapes and MCP schemas at the command boundary. + +## Invocation context and project resolution + +Shared operations receive an immutable invocation context rather than reading +transport globals: + +```text +InvocationContext +├── launch_working_directory +├── project_root +├── allowed_roots +├── access_policy +├── deadline +├── cancellation +└── output_budget +``` + +Project-scoped MCP tools accept an optional project directory when their use +case needs one. If omitted, project discovery starts from the server launch +working directory, matching normal CLI behavior. Resolution uses the same +domain helper as the CLI and produces the same semantic errors. + +The adapter resolves and normalizes paths before invoking the operation: + +- Do not call `os.chdir()` for an MCP request. A long-lived server may process + concurrent or sequential calls with different project contexts. +- Pass the resolved project root explicitly through the operation and its + phases. +- Enforce host-provided allowed roots when available. +- Reject a path outside allowed roots with a structured policy error. +- Do not infer the project from an unrelated server process state after the + invocation begins. + +`init` is a special project-creation operation: its request identifies the +target directory, while the context identifies the launch directory and +allowed roots. + +## Non-interactive behavior + +MCP operations are always non-interactive: + +- They never read stdin. +- They never open arrow-key selectors or terminal confirmations. +- They never wait for an answer that is not represented in the request. +- A safe documented default may be applied only when the CLI operation uses + the same non-interactive default. +- If no safe default exists, return a structured `input_required` or + `confirmation_required` error explaining which field must be supplied. + +Machine mode is not consent. An MCP call, `--json`, `--non-interactive`, a +host confirmation dialog, or an enabled side-effect class does not imply: + +- `force=true`. +- Trust of an external URL or downloaded executable content. +- Permission to overwrite user-modified files. +- Permission to leave the declared project root. +- Permission to execute a workflow, hook, installer, or arbitrary command. + +Consent must be explicit in the command request and valid under the active +access policy. MCP annotations and host UI are advisory; the server still +enforces operation requirements. + +## Side-effect classes and access policy + +Every operation declares the highest applicable side-effect class: + +| Class | Meaning | Representative commands | +| --- | --- | --- | +| `read-only` | Reads local state and returns data without persistent mutation | `version`, `check`, `artifact list` | +| `project-write` | Creates or changes files or configuration within an allowed project/target root | `init`, `extension add`, `preset enable` | +| `execution` | Starts workflows, hooks, agent/tool processes, or other executable behavior | `workflow run`, `workflow resume`, `event run` | +| `self-modifying` | Changes the Specify installation, server runtime, or machine-level state | `self upgrade` | + +Network access is an independent declaration: `none`, `optional`, or +`required`. A read-only search may use the network, while a project-write +operation may be fully offline. + +The MCP server receives an access policy from its host configuration. The +default policy is conservative: + +- `read-only` operations are permitted. +- `project-write`, `execution`, and `self-modifying` operations require + explicit enablement. +- Filesystem access is limited to host-provided roots or, when none are + provided, the server launch working directory. +- Network access is denied unless explicitly enabled. +- External-source trust is separately controlled and remains default-deny. + +Tool annotations should reflect the declared class, but annotations do not +replace server-side enforcement. If policy denies an otherwise implemented +tool, the registered tool returns a structured `policy_denied` error. The +inventory reports its effective `policy-disabled` state and the reason. + +An operation must not relabel itself as read-only merely because writes are +rare, optional, or expected to be idempotent. Classify the most powerful path +the request can activate. + +## Trust, confirmation, and network responsibilities + +The shared operation owns the semantic rule that an action requires trust or +confirmation. The adapters own how explicit consent enters the request. + +- External URL installation remains default-deny without an explicit trust + field. +- A non-empty target directory remains protected without explicit overwrite + consent. +- Catalog discovery permission does not imply install permission. +- A network-enabled policy does not imply trust in arbitrary returned content. +- Redirect, digest, source, and compatibility validation remain domain + behavior, shared by both adapters. +- Transport authentication does not replace operation authorization. + +Network calls use bounded connect/read timeouts and honor the invocation +deadline. Operations do not silently switch from offline to online behavior. +When a request supports offline behavior, the input states it explicitly or +uses the same documented default as the CLI. + +## Timeouts, cancellation, stdin, and bounded output + +The transport adapter creates the invocation deadline and cancellation signal; +the operation and its phases honor them. + +- Subprocesses and network calls receive a timeout derived from the remaining + deadline. +- Complex operations check cancellation between cohesive phases. +- Transactional mutations roll back or report partial state according to + their domain contract. +- Cancellation returns a structured cancellation error, never a successful + empty result. +- No operation reads stdin or inherits an interactive child stdin. +- Captured stdout/stderr and diagnostic details are size-bounded. +- Potentially large lists use command-owned limits or pagination. +- Truncation is explicit and includes a continuation cursor or a clear + `truncated` marker; it is never silent. +- Output-budget enforcement belongs to shared invocation infrastructure, while + pagination semantics belong to the command hierarchy. + +## Transport separation + +`specify_cli/mcp_server/` owns server composition and transport hosting, not +command behavior. + +The initial transport remains stdio: + +- Stdout is reserved for MCP protocol frames. +- Logs and diagnostics use stderr or the SDK's logging channel. +- Startup banners, Rich rendering, and CLI warnings never enter stdout. + +Future Streamable HTTP support should add a transport host around the same +tool registry and operation adapters. HTTP-specific authentication, sessions, +origin checks, request sizing, and connection cancellation belong to that +transport layer. Tool names, schemas, operation contracts, project behavior, +and access classes must not change merely because the transport changes. + +An illustrative infrastructure layout is: + +```text +src/specify_cli/mcp_server/ +├── __init__.py +├── server.py +├── registry.py +├── policy.py +├── context.py +└── transports/ + ├── stdio.py + └── streamable_http.py +``` + +Create only the modules justified by implemented behavior. The layout is a +target boundary, not a requirement to add empty files. + +## Testing structure + +Tests mirror source ownership: + +```text +src/specify_cli/artifacts/_operation_list.py +tests/specify_cli/artifacts/test_operation_list.py + +src/specify_cli/artifacts/command_list.py +tests/specify_cli/artifacts/test_command_list.py + +src/specify_cli/artifacts/mcp_list.py +tests/specify_cli/artifacts/test_mcp_list.py +``` + +Private operation phases use: + +```text +src/specify_cli/_operation_init_validation.py +tests/specify_cli/test_operation_init_validation.py +``` + +The required test layers are: + +### Operation tests + +- Cover valid requests and intended results. +- Cover invalid inputs, prevented behavior, and domain failures. +- Verify warnings, typed errors, side effects, rollback, cancellation, and + bounded behavior where applicable. +- Avoid Typer, MCP transport, and Rich assertions. + +### CLI adapter tests + +- Verify argument and option mapping. +- Verify prompts and non-interactive behavior. +- Verify human rendering, JSON streams, and exit codes. +- Preserve established help and compatibility import paths. + +### MCP adapter tests + +- Verify tool name, description, annotations, and exact input/output schemas. +- Verify mapping to the shared operation request and outcome. +- Verify structured warnings and tool errors. +- Verify access-policy, trust, timeout, cancellation, and output-budget + failures. + +### Inventory and parity tests + +- Walk the actual CLI command tree and require one MCP inventory disposition + for every leaf. +- Reject duplicate operation IDs and MCP tool names. +- Require reasons for every deferred or excluded command. +- Verify available tools are registered by the owning hierarchy. +- Run CLI JSON and MCP adapters against the same operation fixture and compare + semantic result, warning, error, and side-effect behavior. +- Preserve total pytest collection when tests move, as required by the CLI + architecture. + +Adapter parity does not require byte-identical human terminal output. It +requires both adapters to invoke the same operation contract with equivalent +inputs and to represent the same outcome without inventing behavior. + +### Protocol tests + +- Keep an in-memory MCP registration and dispatch test. +- Keep a real stdio initialize/list/call test with protocol-pure stdout. +- Add equivalent Streamable HTTP protocol, authentication, cancellation, and + isolation tests when that transport exists. +- Test malformed input, policy denial, unavailable tools, internal failure + sanitization, and output bounds as negative cases. + +Behavioral changes follow +[Testing deterministic behavior](../CONTRIBUTING.md#testing-deterministic-behavior): +positive and negative evidence is required, and bug fixes need before-and-after +regression evidence. + +## Incremental migration + +Migration proceeds by operation, preserving the experimental server until +first-class replacements are verified. + +1. **Introduce shared primitives and inventory.** Add invocation context, + policy, warning/error primitives, and explicit per-leaf dispositions + without changing supported tools. +2. **Extract `version`.** Move version collection into a typed shared + operation used by both `command_version.py` and `mcp_version.py`. Register + `specify_version` alongside the transitional generic tools and prove output + parity. +3. **Add project-scoped reads.** Migrate `artifact list`, then adjacent + artifact inspection operations, establishing project-root and bounded + output behavior. +4. **Migrate bounded mutations.** Extract shared operations for focused + project-write commands, preserving CLI behavior and adding explicit policy + and consent tests. +5. **Migrate complex execution and initialization.** Refactor cohesive phases + below both adapters. Migrate `init`, workflow execution, and similar + commands only after cancellation, rollback, trust, and timeout contracts + are explicit. +6. **Retire subprocess dispatch.** Remove per-operation child-process + invocation after every command supported by the generic runner has a + first-class tool and compatibility window. +7. **Deprecate the generic run tool.** Keep inventory/describe diagnostics if + useful, but remove generic execution from the target surface. +8. **Add Streamable HTTP.** Reuse the same registry and adapters; add only + transport-specific hosting and security behavior. + +Migration must preserve established CLI imports and monkeypatch paths through +thin forwarders when required. Do not combine architecture migration with +unrelated command behavior changes. + +## Representative operation layouts + +### `version`: simple, process-scoped read + +```text +src/specify_cli/ +├── _version.py +├── command_version.py +└── mcp_version.py + +tests/specify_cli/ +├── test_operation_version.py +├── test_command_version.py +└── test_mcp_version.py +``` + +Contract: + +```text +operation_id: version +cli_path: specify version +mcp_tool_name: specify_version +side_effect_class: read-only +network_access: none +project_scope: process +``` + +`_version.py` owns typed version collection and `VersionResult`. +`command_version.py` renders the panel, feature text, or established JSON +object. `mcp_version.py` returns the same result fields as structured content. +No adapter starts a child process. + +### `artifact list`: project-scoped read + +```text +src/specify_cli/artifacts/ +├── __init__.py +├── _commands.py +├── _mcp.py +├── _operation_list.py +├── command_list.py +└── mcp_list.py + +tests/specify_cli/artifacts/ +├── test_operation_list.py +├── test_command_list.py +└── test_mcp_list.py +``` + +Contract: + +```text +operation_id: artifact.list +cli_path: specify artifact list +mcp_tool_name: specify_artifact_list +side_effect_class: read-only +network_access: none +project_scope: required +``` + +The request contains an optional project directory. Project resolution +produces a normalized root in the invocation context. `_operation_list.py` +uses `ArtifactCatalog` and returns typed artifact rows. The CLI adapter +preserves its JSON stream contract; the MCP adapter exposes the rows through +its output schema and never captures CLI stdout. Invocation output budgets +must produce explicit bounded-output behavior rather than silent truncation. + +### `init`: complex project mutation + +```text +src/specify_cli/ +├── command_init.py +├── mcp_init.py +├── _operation_init.py +├── _operation_init_validation.py +├── _operation_init_plan.py +├── _operation_init_apply.py +└── _operation_init_finalize.py + +tests/specify_cli/ +├── test_command_init.py +├── test_mcp_init.py +├── test_operation_init.py +├── test_operation_init_validation.py +├── test_operation_init_plan.py +├── test_operation_init_apply.py +└── test_operation_init_finalize.py +``` + +This remains at the root; an `init/` directory would incorrectly imply a +`specify init ...` nested command group. + +Contract: + +```text +operation_id: init +cli_path: specify init +mcp_tool_name: specify_init +side_effect_class: project-write +network_access: optional +project_scope: creates-target +``` + +The request explicitly carries the target, integration, script type, optional +preset/extensions, `force`, and external-URL trust decision. The MCP adapter +never prompts and never turns its machine context into force or trust. The +shared operation validates inputs, builds a plan, applies transactional +changes, and returns created/updated paths plus structured warnings. The CLI +adapter may gather interactive choices before constructing the same request. + +If the target is non-empty and `force` is false, both adapters receive the same +semantic confirmation-required failure. The CLI may respond by prompting and +retrying with explicit consent; the MCP tool returns the structured error and +requires a new call with `force=true`, subject to policy. + +## Anti-patterns + +Avoid: + +- MCP calling Typer handlers directly. +- MCP starting the human CLI for normal dispatch. +- Parsing Rich output, terminal text, or CLI stderr to recover results. +- Duplicating command orchestration in an MCP adapter. +- Defining command-specific request and result models in a central MCP + catalog. +- Hiding behavior behind a central service locator or string-based dispatcher. +- Exposing every operation through one generic run tool. +- Forcing every command into an oversized universal execution engine. +- Treating MCP tool annotations as authorization. +- Treating machine mode as force, trust, overwrite consent, or execution + permission. +- Reading stdin or changing process-wide cwd during a tool call. +- Auto-registering tools by scanning files or importing every + `command_*.py`. +- Creating an MCP directory tree that duplicates and detaches the CLI/domain + hierarchy. +- Adding operation phases or `_mcp.py` files solely for symmetry. +- Silently omitting CLI leaves from the MCP inventory. +- Returning partial, truncated, or fallback data as a successful complete + result. +- Letting transport concerns leak into command contracts. + +## Review checklist + +For a new or migrated MCP operation: + +- [ ] The CLI leaf and MCP tool map to one logical operation ID. +- [ ] Both adapters dispatch into the same typed shared implementation. +- [ ] The MCP tool is first-class and has a command-specific schema. +- [ ] The tool name and source layout mirror the CLI path. +- [ ] The owning command hierarchy declares registration and inventory. +- [ ] Availability, side-effect class, network access, project scope, and + contract version are explicit. +- [ ] Non-interactive behavior does not imply force, trust, or consent. +- [ ] Project paths are normalized and passed explicitly without `os.chdir()`. +- [ ] Timeouts, cancellation, stdin, and output bounds are handled. +- [ ] Existing CLI human and JSON behavior remains compatible. +- [ ] Operation, CLI adapter, MCP adapter, parity, and protocol tests cover + positive and negative behavior. +- [ ] Stdio remains protocol-pure, and command behavior is transport-neutral. +- [ ] No command behavior was added to central MCP infrastructure. diff --git a/docs/guides/agentic-sdlc.md b/docs/guides/agentic-sdlc.md index eafa14da61..d9c6468c95 100644 --- a/docs/guides/agentic-sdlc.md +++ b/docs/guides/agentic-sdlc.md @@ -105,6 +105,7 @@ Larger changes need prior discussion and agreement with maintainers, as the [contribution guide](https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#submitting-a-pull-request) explains. When decisions should remain useful across changes, the repository keeps [CLI](https://github.com/github/spec-kit/blob/main/design/cli.md), +[MCP](https://github.com/github/spec-kit/blob/main/design/mcp.md), [integration](https://github.com/github/spec-kit/blob/main/design/integration.md), and [workflow-step](https://github.com/github/spec-kit/blob/main/design/workflow-step.md) design documents. diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index cf709c03a4..56bf7af29f 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -4,6 +4,11 @@ > `specify mcp` is experimental. Its command inventory and tool contracts may > change before the MCP surface is declared stable. +This page documents the current transitional implementation. See +[Specify MCP Command Architecture](https://github.com/github/spec-kit/blob/main/design/mcp.md) +for the intended first-class tool, shared-operation, policy, and transport +architecture. + ```bash specify mcp ``` From 3b98377d6ecf3552ca64bfbf4b003941cffb1743 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:11:15 -0500 Subject: [PATCH 02/19] docs: model MCP access as capabilities Replace the single highest side-effect class with cumulative capability requirements, classify check as requiring execution, and document request-specific capability evaluation. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 91 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index ffeccdf7a5..591d406119 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -331,7 +331,7 @@ mcp_tool_name contract_version availability availability_reason -side_effect_class +capabilities network_access project_scope default_timeout @@ -361,16 +361,20 @@ MCP tool that starts another MCP server would be recursive infrastructure, not an application operation. Other commands are not excluded merely because they mutate state. They are -classified and gated. For example: +declared and gated by capability. For example: - `self.upgrade` is self-modifying and should be unavailable under the default local policy. Exposure requires an explicit administrative policy - and a command contract that preserves upgrade safeguards. + authorizing local reads, execution, and self-modification, plus a command + contract that preserves upgrade safeguards. - `event.run`, `workflow.run`, and `workflow.resume` are execution operations - and require execution policy. + that may also persist project state; policy must authorize every capability + required by the operation. - `init`, add/remove/update commands, and configuration changes are - project-write operations, with any stronger capabilities declared - separately. + project-write operations, with execution and other independent + capabilities declared when their paths require them. +- `check` launches installed host tools to inspect their versions, so it + requires execution capability even though it does not persist changes. - A command that still prompts, writes directly through its Typer handler, or lacks a typed result remains `deferred` until those concerns move into a shared operation. @@ -519,7 +523,7 @@ MCP operations are always non-interactive: `confirmation_required` error explaining which field must be supplied. Machine mode is not consent. An MCP call, `--json`, `--non-interactive`, a -host confirmation dialog, or an enabled side-effect class does not imply: +host confirmation dialog, or an authorized capability set does not imply: - `force=true`. - Trust of an external URL or downloaded executable content. @@ -531,17 +535,44 @@ Consent must be explicit in the command request and valid under the active access policy. MCP annotations and host UI are advisory; the server still enforces operation requirements. -## Side-effect classes and access policy +## Capability requirements and access policy -Every operation declares the highest applicable side-effect class: +Every operation declares a set of independent capabilities. Policy must +authorize every capability required by the validated request; choosing one +"highest" class is not sufficient. -| Class | Meaning | Representative commands | +| Capability | Meaning | Representative commands | | --- | --- | --- | -| `read-only` | Reads local state and returns data without persistent mutation | `version`, `check`, `artifact list` | +| `local-read` | Reads process, host, or project state within allowed roots without persistent mutation | `version`, `artifact list` | | `project-write` | Creates or changes files or configuration within an allowed project/target root | `init`, `extension add`, `preset enable` | -| `execution` | Starts workflows, hooks, agent/tool processes, or other executable behavior | `workflow run`, `workflow resume`, `event run` | +| `execution` | Starts host tools, workflows, hooks, agent/tool processes, or other executable behavior | `check`, `workflow run`, `workflow resume`, `event run` | | `self-modifying` | Changes the Specify installation, server runtime, or machine-level state | `self upgrade` | +Capabilities are cumulative requirements, not a hierarchy with implied +permissions. Examples: + +```text +version -> {local-read} +check -> {local-read, execution} +workflow.run -> {local-read, project-write, execution} +self.upgrade -> {local-read, execution, self-modifying} +``` + +The inventory declares the conservative set of capabilities any request for +the operation may require. When options activate materially different paths, +the command-owned operation may implement +`required_capabilities(request) -> set[Capability]` to compute the exact +subset after semantic validation. For example, `init` declares +`{local-read, project-write, execution}` because its normal tool checks launch +host binaries; a validated request that explicitly skips those checks may not +require `execution`. The MCP adapter and access-policy layer must not +independently infer or reduce the set. + +`read-only` is a derived description, not an authorizable capability. A +request is read-only only when it requires no `project-write`, `execution`, or +`self-modifying` capability. An operation that launches a binary is therefore +not read-only even if it does not persist changes. + Network access is an independent declaration: `none`, `optional`, or `required`. A read-only search may use the network, while a project-write operation may be fully offline. @@ -549,22 +580,26 @@ operation may be fully offline. The MCP server receives an access policy from its host configuration. The default policy is conservative: -- `read-only` operations are permitted. -- `project-write`, `execution`, and `self-modifying` operations require - explicit enablement. +- `local-read` is authorized. +- `project-write`, `execution`, and `self-modifying` require explicit + authorization. - Filesystem access is limited to host-provided roots or, when none are provided, the server launch working directory. - Network access is denied unless explicitly enabled. - External-source trust is separately controlled and remains default-deny. -Tool annotations should reflect the declared class, but annotations do not -replace server-side enforcement. If policy denies an otherwise implemented -tool, the registered tool returns a structured `policy_denied` error. The -inventory reports its effective `policy-disabled` state and the reason. +Tool annotations should conservatively reflect the full declared capability +set, but annotations do not replace server-side enforcement. If policy denies +any capability required by an otherwise implemented tool request, the +registered tool returns a structured `policy_denied` error identifying the +missing capabilities. The inventory reports its effective `policy-disabled` +state and the reason. -An operation must not relabel itself as read-only merely because writes are -rare, optional, or expected to be idempotent. Classify the most powerful path -the request can activate. +An operation must not omit a capability merely because the path is rare, +optional, expected to be idempotent, or combined with a more powerful +capability. The static declaration contains the union of possible +requirements; request-specific evaluation may only narrow it from validated +inputs. ## Trust, confirmation, and network responsibilities @@ -621,7 +656,8 @@ Future Streamable HTTP support should add a transport host around the same tool registry and operation adapters. HTTP-specific authentication, sessions, origin checks, request sizing, and connection cancellation belong to that transport layer. Tool names, schemas, operation contracts, project behavior, -and access classes must not change merely because the transport changes. +and capability requirements must not change merely because the transport +changes. An illustrative infrastructure layout is: @@ -773,7 +809,7 @@ Contract: operation_id: version cli_path: specify version mcp_tool_name: specify_version -side_effect_class: read-only +capabilities: [local-read] network_access: none project_scope: process ``` @@ -806,7 +842,7 @@ Contract: operation_id: artifact.list cli_path: specify artifact list mcp_tool_name: specify_artifact_list -side_effect_class: read-only +capabilities: [local-read] network_access: none project_scope: required ``` @@ -849,7 +885,7 @@ Contract: operation_id: init cli_path: specify init mcp_tool_name: specify_init -side_effect_class: project-write +capabilities: [local-read, project-write, execution] network_access: optional project_scope: creates-target ``` @@ -902,7 +938,8 @@ For a new or migrated MCP operation: - [ ] The MCP tool is first-class and has a command-specific schema. - [ ] The tool name and source layout mirror the CLI path. - [ ] The owning command hierarchy declares registration and inventory. -- [ ] Availability, side-effect class, network access, project scope, and +- [ ] Availability, possible and request-required capabilities, network + access, project scope, and contract version are explicit. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. From f02468d11425e7b6f390df8466381912f38ced07 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:11:56 -0500 Subject: [PATCH 03/19] docs: narrow MCP architecture scope Remove the unrequested guide and reference cross-links while retaining the authoritative design document and contributor guidance. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/guides/agentic-sdlc.md | 1 - docs/reference/mcp.md | 5 ----- 2 files changed, 6 deletions(-) diff --git a/docs/guides/agentic-sdlc.md b/docs/guides/agentic-sdlc.md index d9c6468c95..eafa14da61 100644 --- a/docs/guides/agentic-sdlc.md +++ b/docs/guides/agentic-sdlc.md @@ -105,7 +105,6 @@ Larger changes need prior discussion and agreement with maintainers, as the [contribution guide](https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#submitting-a-pull-request) explains. When decisions should remain useful across changes, the repository keeps [CLI](https://github.com/github/spec-kit/blob/main/design/cli.md), -[MCP](https://github.com/github/spec-kit/blob/main/design/mcp.md), [integration](https://github.com/github/spec-kit/blob/main/design/integration.md), and [workflow-step](https://github.com/github/spec-kit/blob/main/design/workflow-step.md) design documents. diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index 56bf7af29f..cf709c03a4 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -4,11 +4,6 @@ > `specify mcp` is experimental. Its command inventory and tool contracts may > change before the MCP surface is declared stable. -This page documents the current transitional implementation. See -[Specify MCP Command Architecture](https://github.com/github/spec-kit/blob/main/design/mcp.md) -for the intended first-class tool, shared-operation, policy, and transport -architecture. - ```bash specify mcp ``` From 1b01539378ab6c1eaffd4a61769fa34f1c92dacf Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:14:00 -0500 Subject: [PATCH 04/19] docs: make MCP architecture normative Remove current-state and adoption framing so the design document describes only the required architecture and conformance rules. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 110 ++++++++++++++------------------------------------ 1 file changed, 31 insertions(+), 79 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index 591d406119..f6020fe903 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -1,16 +1,10 @@ # Specify MCP Command Architecture -This document defines the target architecture for exposing Specify operations -through the Model Context Protocol (MCP). It is the MCP counterpart to +This document defines the architecture for exposing Specify operations through +the Model Context Protocol (MCP). It is the MCP counterpart to [Specify CLI Command Architecture](cli.md): command paths, ownership, -registration, contracts, tests, and migration should be predictable from the -surface being changed. - -The current `src/specify_cli/mcp_server/` implementation is intentionally -experimental and transitional. It exposes only `version` through generic -list, describe, and run tools, invokes the CLI in a child process, and parses -the CLI JSON result. That is a bounded first implementation, not the target -architecture described here. +registration, contracts, tests, and operational policy should be predictable +from the surface being changed. ## Design goals @@ -33,8 +27,6 @@ The design optimizes for: decisions, and self-modification are declared and enforced. - **Transport independence:** stdio and future Streamable HTTP hosting do not change command behavior. -- **Incremental migration:** commands move to the shared model without a flag - day or compatibility break. ## Non-goals @@ -76,9 +68,9 @@ The CLI and MCP adapters are peers: - The shared operation owns semantic validation, orchestration, side effects, typed results, warnings, and structured domain errors. -Neither adapter calls the other. In particular, the target MCP adapter must -not invoke Typer handlers, start `specify` as a child process, scrape Rich -output, or parse CLI stderr. +Neither adapter calls the other. In particular, the MCP adapter must not invoke +Typer handlers, start `specify` as a child process, scrape Rich output, or +parse CLI stderr. The shared operation is the behavioral source of truth. Adapters may differ in presentation, but they must not differ in what the operation means. @@ -129,11 +121,11 @@ the reason. ### First-class tools, not a generic execution facade -The target surface exposes each *eligible* CLI leaf as a first-class MCP tool. -A generic `specify_run_command` facade is not the target. +The MCP surface exposes each *eligible* CLI leaf as a first-class MCP tool. +A generic `specify_run_command` facade is not part of the architecture. -The current MCP SDK creates one JSON input schema per registered tool from the -tool's typed callable. First-class tools therefore preserve: +The MCP SDK creates one JSON input schema per registered tool from the tool's +typed callable. First-class tools therefore preserve: - Per-command schemas and descriptions. - MCP client discovery and argument validation. @@ -153,9 +145,9 @@ per command hierarchy, but not a reason to erase command-specific contracts behind a generic tool. Tests should derive the current count rather than hard-code 90. -### Current transitional inventory +### Command inventory -The point-in-time CLI leaf inventory used to establish this design is: +The CLI leaf inventory at the time this design was established is: ```text root: init, check, version, mcp @@ -181,23 +173,17 @@ workflow.step.catalog: add, list, remove workflow.overlay: add, set-priority, enable, disable, remove, list ``` -In the current experimental server: - -- `version` is available only through the transitional generic tools and maps - to the target first-class tool `specify_version`. -- `mcp` is excluded because it is the transport host. -- The other 88 leaves are unavailable through MCP. Their initial target - disposition is `deferred` until their shared operation, typed contract, and - access-policy behavior satisfy this design. +`mcp` is excluded because it is the transport host. Every other available leaf +maps to a first-class tool using the naming rule above. A leaf that is +unavailable or excluded must still have an explicit hierarchy-owned inventory +record and reason. -This list records the migration baseline, not a second registration source. -Once implemented, the hierarchy-owned inventory and its parity tests are -authoritative. +This list documents the command namespaces; it is not a registration source. +The hierarchy-owned inventory and its parity tests are authoritative. -Metadata-only inventory or describe tools may remain for compatibility or -diagnostics. They do not replace first-class operation tools, and a generic -run tool should be deprecated after migrated tools cover its supported -operations. +Metadata-only inventory or describe tools may exist for diagnostics. They do +not replace first-class operation tools. A generic execution tool is not part +of this architecture. ## Naming and file layout @@ -340,8 +326,8 @@ default_timeout The static disposition values are: - `available`: implemented and registered as a first-class tool. -- `deferred`: the CLI leaf exists, but shared operation extraction or a typed - MCP contract is incomplete. +- `unavailable`: the logical operation is known but cannot be offered in the + current distribution or platform; the record states the concrete reason. - `excluded`: the command is intentionally not an MCP operation. An available tool may have an effective runtime state of `policy-disabled`. @@ -375,9 +361,8 @@ declared and gated by capability. For example: capabilities declared when their paths require them. - `check` launches installed host tools to inspect their versions, so it requires execution capability even though it does not persist changes. -- A command that still prompts, writes directly through its Typer handler, or - lacks a typed result remains `deferred` until those concerns move into a - shared operation. +- A command that still depends on prompts, writes directly through its Typer + handler, or lacks a typed result must not be marked `available`. Exclusion and deferral are reviewable architecture decisions, not silent omissions. @@ -467,7 +452,8 @@ Rules for contract evolution: - Backward-compatible optional fields and new warning codes may retain the current major contract version. - Removing, renaming, or changing the meaning of an input, output, warning, - or error requires a new major contract version and a migration plan. + or error requires a new major contract version and an explicit compatibility + strategy. - CLI and MCP adapters for the same operation advertise the same contract version. - An adapter-only presentation change does not change the operation contract @@ -673,8 +659,8 @@ src/specify_cli/mcp_server/ └── streamable_http.py ``` -Create only the modules justified by implemented behavior. The layout is a -target boundary, not a requirement to add empty files. +Create only the modules justified by implemented behavior. The layout defines +an architectural boundary, not a requirement to add empty files. ## Testing structure @@ -728,7 +714,7 @@ The required test layers are: - Walk the actual CLI command tree and require one MCP inventory disposition for every leaf. - Reject duplicate operation IDs and MCP tool names. -- Require reasons for every deferred or excluded command. +- Require reasons for every unavailable or excluded command. - Verify available tools are registered by the owning hierarchy. - Run CLI JSON and MCP adapters against the same operation fixture and compare semantic result, warning, error, and side-effect behavior. @@ -753,40 +739,6 @@ Behavioral changes follow positive and negative evidence is required, and bug fixes need before-and-after regression evidence. -## Incremental migration - -Migration proceeds by operation, preserving the experimental server until -first-class replacements are verified. - -1. **Introduce shared primitives and inventory.** Add invocation context, - policy, warning/error primitives, and explicit per-leaf dispositions - without changing supported tools. -2. **Extract `version`.** Move version collection into a typed shared - operation used by both `command_version.py` and `mcp_version.py`. Register - `specify_version` alongside the transitional generic tools and prove output - parity. -3. **Add project-scoped reads.** Migrate `artifact list`, then adjacent - artifact inspection operations, establishing project-root and bounded - output behavior. -4. **Migrate bounded mutations.** Extract shared operations for focused - project-write commands, preserving CLI behavior and adding explicit policy - and consent tests. -5. **Migrate complex execution and initialization.** Refactor cohesive phases - below both adapters. Migrate `init`, workflow execution, and similar - commands only after cancellation, rollback, trust, and timeout contracts - are explicit. -6. **Retire subprocess dispatch.** Remove per-operation child-process - invocation after every command supported by the generic runner has a - first-class tool and compatibility window. -7. **Deprecate the generic run tool.** Keep inventory/describe diagnostics if - useful, but remove generic execution from the target surface. -8. **Add Streamable HTTP.** Reuse the same registry and adapters; add only - transport-specific hosting and security behavior. - -Migration must preserve established CLI imports and monkeypatch paths through -thin forwarders when required. Do not combine architecture migration with -unrelated command behavior changes. - ## Representative operation layouts ### `version`: simple, process-scoped read From 2fe781c48de36a389ff947927bcee17688b24c9b Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:21:37 -0500 Subject: [PATCH 05/19] docs: tighten MCP contract boundaries Authorize capabilities before stateful validation, separate static disposition from runtime policy state, keep version in its own operation module, and make errors and contract versions transport-neutral. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 104 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index f6020fe903..c180ccbb47 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -244,11 +244,11 @@ module: ```text command_version.py mcp_version.py -_version.py +_operation_version.py ``` -Both adapters map to the typed operation in `_version.py`. No extra phase -module is required. +Both adapters map to the typed operation in `_operation_version.py`. No extra +phase module is required. ### Complex operation @@ -299,7 +299,7 @@ For a group such as `artifact`: 1. `artifacts/_mcp.py` lists every `artifact.*` CLI leaf. 2. Each leaf has exactly one inventory record. 3. Available records import and register their `mcp_.py` adapter. -4. Deferred, policy-disabled, and excluded records state a reason. +4. Unavailable and excluded records state a reason. 5. The root MCP composition module calls `artifacts._mcp.register(...)`. The root MCP server may aggregate hierarchy registration functions, but it @@ -315,8 +315,8 @@ operation_id cli_path mcp_tool_name contract_version -availability -availability_reason +disposition +disposition_reason capabilities network_access project_scope @@ -330,11 +330,17 @@ The static disposition values are: current distribution or platform; the record states the concrete reason. - `excluded`: the command is intentionally not an MCP operation. -An available tool may have an effective runtime state of `policy-disabled`. -It remains discoverable with its typed schema and annotations, but invocation -returns a structured `policy_denied` error. This keeps discovery stable across -policy profiles and prevents a host configuration change from changing tool -identity. +Runtime policy state is separate from static inventory disposition. An +available tool has an `effective_state` of: + +- `enabled`: the active policy authorizes the request's required + capabilities. +- `policy-disabled`: the tool remains discoverable, but invocation returns a + structured `policy_denied` error identifying the missing authorization. + +`effective_state` is derived from the active policy and request; it is not +stored as the inventory's static disposition. A metadata or describe surface +may report both fields but must preserve their distinct types. Every CLI leaf must appear exactly once. The inventory parity test fails for a missing leaf, duplicate logical operation, duplicate tool name, stale CLI @@ -349,10 +355,10 @@ an application operation. Other commands are not excluded merely because they mutate state. They are declared and gated by capability. For example: -- `self.upgrade` is self-modifying and should be unavailable under the - default local policy. Exposure requires an explicit administrative policy - authorizing local reads, execution, and self-modification, plus a command - contract that preserves upgrade safeguards. +- `self.upgrade` has static disposition `available` when its first-class tool + is implemented. Its effective state is `policy-disabled` under the default + policy because local reads, execution, and self-modification are not all + authorized. - `event.run`, `workflow.run`, and `workflow.resume` are execution operations that may also persist project state; policy must authorize every capability required by the operation. @@ -364,7 +370,7 @@ declared and gated by capability. For example: - A command that still depends on prompts, writes directly through its Typer handler, or lacks a typed result must not be marked `available`. -Exclusion and deferral are reviewable architecture decisions, not silent +Unavailability and exclusion are reviewable architecture decisions, not silent omissions. ## Command contracts @@ -382,12 +388,38 @@ The model: `trust_extension_urls` only when the operation supports them. The CLI adapter maps parsed arguments and options into the request. The MCP -adapter exposes a command-specific JSON schema and maps validated tool input -into the same request. - -Typer usage errors remain CLI concerns. Semantic errors such as an unknown -integration, invalid project state, or incompatible options belong to the -operation so both adapters report the same failure. +adapter exposes a command-specific JSON schema and maps schema-validated tool +input into the same request. + +Typer usage errors remain CLI concerns. Pure request errors and state-dependent +semantic errors belong to the operation so both adapters report the same +failure. + +### Validation and authorization order + +Capability authorization occurs before any state-dependent validation. The +invocation sequence is: + +1. The adapter parses and schema-validates transport input without filesystem, + network, environment, or process access. +2. The operation performs capability-free request validation. It may check + types, enum values, mutually exclusive fields, required combinations, and + other invariants derived solely from request values and static operation + metadata. +3. The operation computes `required_capabilities(request)` and request-specific + network requirements. This computation is pure and performs no I/O. +4. The access-policy layer authorizes every computed capability, network + requirement, and requested root. A denial stops the invocation. +5. Only after authorization may the operation resolve project state and run + state-dependent semantic validation, such as checking an integration, + reading project files, consulting a catalog, or inspecting an installed + tool. +6. The operation performs its side effects and returns its typed outcome. + +The capability computation must conservatively cover every path reachable +from the validated request. Stateful validation must not discover and then +exercise an additional unauthorized capability. If an invariant cannot be +checked without a capability, the check belongs after authorization. ### Typed results and warnings @@ -416,14 +448,14 @@ code message details retryable -exit_code ``` The operation hierarchy owns the error code and details schema. The CLI adapter maps it to human output or the command's JSON failure contract and -then uses the declared exit code. The MCP adapter maps it to an MCP tool error -with structured content. Neither adapter exposes a traceback, secret, raw -subprocess output, or success-shaped fallback. +maps stable operation error codes to established CLI exit codes. The MCP +adapter maps it to an MCP tool error with structured content. Neither adapter +exposes a traceback, secret, raw subprocess output, or success-shaped +fallback. Unexpected exceptions are normalized at the adapter boundary to a sanitized `internal_error`, logged only through the transport-appropriate diagnostic @@ -454,8 +486,11 @@ Rules for contract evolution: - Removing, renaming, or changing the meaning of an input, output, warning, or error requires a new major contract version and an explicit compatibility strategy. -- CLI and MCP adapters for the same operation advertise the same contract - version. +- Both adapters conform to the contract version declared by the + hierarchy-owned inventory. MCP exposes that version through tool or + inventory metadata; CLI contract tests reference the same declaration and + lock its machine result, warning, and error shapes without adding a new CLI + output field. - An adapter-only presentation change does not change the operation contract version. - Tests lock established JSON shapes and MCP schemas at the command boundary. @@ -548,7 +583,9 @@ The inventory declares the conservative set of capabilities any request for the operation may require. When options activate materially different paths, the command-owned operation may implement `required_capabilities(request) -> set[Capability]` to compute the exact -subset after semantic validation. For example, `init` declares +subset after capability-free request validation. The computation uses only +request values and static operation metadata and must perform no filesystem, +network, environment, or process access. For example, `init` declares `{local-read, project-write, execution}` because its normal tool checks launch host binaries; a validated request that explicitly skips those checks may not require `execution`. The MCP adapter and access-policy layer must not @@ -578,8 +615,9 @@ Tool annotations should conservatively reflect the full declared capability set, but annotations do not replace server-side enforcement. If policy denies any capability required by an otherwise implemented tool request, the registered tool returns a structured `policy_denied` error identifying the -missing capabilities. The inventory reports its effective `policy-disabled` -state and the reason. +missing capabilities. A runtime describe surface may report +`effective_state: policy-disabled`; the static inventory disposition remains +`available`. An operation must not omit a capability merely because the path is rare, optional, expected to be idempotent, or combined with a more powerful @@ -745,7 +783,7 @@ regression evidence. ```text src/specify_cli/ -├── _version.py +├── _operation_version.py ├── command_version.py └── mcp_version.py @@ -766,7 +804,7 @@ network_access: none project_scope: process ``` -`_version.py` owns typed version collection and `VersionResult`. +`_operation_version.py` owns typed version collection and `VersionResult`. `command_version.py` renders the panel, feature text, or established JSON object. `mcp_version.py` returns the same result fields as structured content. No adapter starts a child process. From 055efbe0e7388ee25ae757bd70ef70fd98894d22 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:28:54 -0500 Subject: [PATCH 06/19] docs: separate shared command architecture Define the common application layer beneath CLI and MCP adapters and narrow each adapter design to its own delivery concerns. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 15 +- design/cli.md | 104 ++++++++----- design/mcp.md | 388 ++++++++-------------------------------------- design/shared.md | 395 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 539 insertions(+), 363 deletions(-) create mode 100644 design/shared.md diff --git a/AGENTS.md b/AGENTS.md index 704f45699c..d976a41905 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,17 +11,18 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their ## Adding or Updating CLI Commands Before adding, updating, or reorganizing Specify CLI commands, read -[Specify CLI Command Architecture](design/cli.md). It defines command-module -naming, private command phases, nested command groups, registration ownership, -mirrored tests, and the rationale for making the CLI structure predictable for -both humans and coding agents. +[Shared Command Application Architecture](design/shared.md) and +[Specify CLI Command Architecture](design/cli.md). They define the shared +operation boundary, command-module naming, private phases, nested command +groups, registration ownership, and mirrored tests. ## Adding or Updating MCP Commands Before adding or changing MCP tools for Specify commands, read -[Specify MCP Command Architecture](design/mcp.md). It defines the shared -operation boundary, peer CLI/MCP adapters, typed contracts, explicit inventory, -access policy, transport separation, and mirrored tests. +[Shared Command Application Architecture](design/shared.md) and +[Specify MCP Command Architecture](design/mcp.md). They define the shared +operation boundary, typed contracts, explicit inventory, access policy, +transport separation, and mirrored tests. ## Adding or Updating Agent Integrations diff --git a/design/cli.md b/design/cli.md index b38dfec09f..0b88b6b971 100644 --- a/design/cli.md +++ b/design/cli.md @@ -1,12 +1,18 @@ # Specify CLI Command Architecture -This document defines the target structure for multi-command groups in the -Specify Python CLI. It explains where command handlers, shared infrastructure, -command-private phases, nested command groups, and their tests belong. +This document defines the CLI adapter structure for Specify commands. It +explains where Typer handlers, CLI infrastructure, CLI-private phases, nested +command groups, and their tests belong. -`src/specify_cli/extensions/` is the reference implementation. Apply this -design incrementally when adding or refactoring other command groups; do not -create extra modules merely to make a small command conform visually. +Every CLI leaf that has another delivery adapter invokes the shared application +operation defined by +[Shared Command Application Architecture](shared.md). This document owns the +CLI surface; it does not redefine semantic validation, orchestration, results, +warnings, errors, or side effects. + +The extension hierarchy supplies the reference command-group shape used in +this document. Do not create extra modules merely to make a small command +conform visually. ## Design goals @@ -22,6 +28,8 @@ The design optimizes for: the same file. - **Explicit ownership:** shared infrastructure and command-private behavior should not be mixed. +- **Adapter discipline:** CLI modules invoke shared operations rather than + becoming the application implementation. - **Stable behavior:** structural refactoring must preserve registration, output, error handling, compatibility paths, and tests. - **Agentic development:** coding agents should be able to infer the relevant @@ -50,17 +58,24 @@ Only modules representing actual CLI commands use the non-underscored - The Typer-decorated handler. - User-facing arguments and options. -- Command-specific orchestration. +- Mapping parsed values into the shared operation request. +- CLI prompting, progress, rendering, JSON streams, and exit-code mapping. - Small helpers used only by that command. +Semantic validation, application orchestration, typed outcomes, warnings, +expected errors, and side effects belong below the adapter as defined in +[the shared design](shared.md). A CLI-only command may keep small behavior in +its command module, but behavior needed by another adapter must first move to a +shared operation or domain module. + The command function's docstring is user-facing because Typer may display it as help text. A module docstring is internal and should identify the command, registration path, and any adjacent private implementation modules. ### Command-private implementation modules -When a command has cohesive phases that are independently understandable or -testable, use: +When a CLI adapter has cohesive *CLI-specific* phases that are independently +understandable or testable, use: ```text _command__.py @@ -69,10 +84,9 @@ _command__.py For example: ```text -command_update.py -_command_update_discovery.py -_command_update_artifacts.py -_command_update_transaction.py +command_init.py +_command_init_prompting.py +_command_init_rendering.py ``` The leading underscore marks the module as private implementation. The @@ -84,11 +98,16 @@ Private phase modules must not register additional CLI commands. The public Split a command when a phase: +- Is specific to CLI invocation or presentation. - Has distinct invariants or failure behavior. - Can be tested as a meaningful boundary. - Has enough implementation detail to distract from the CLI handler. - Is likely to change independently from other phases. +If the phase performs semantic validation, planning, mutation, rollback, or +other behavior another adapter needs, use `_operation__.py` +instead, following [the shared design](shared.md#naming-and-layout). + Do not split a command solely because it crossed an arbitrary line count. Excessive fragmentation makes control flow harder to follow and increases the number of files an agent must inspect. @@ -99,7 +118,8 @@ For a multi-command group, `_commands.py` owns: - The command group's Typer application. - Registration of the group's command modules. -- Infrastructure genuinely shared by multiple commands or external CLI flows. +- CLI infrastructure genuinely shared by multiple commands or external CLI + flows. - Thin compatibility forwarders needed to preserve established import or monkeypatch paths. @@ -215,23 +235,22 @@ src/specify_cli/extensions/catalog/command_add.py tests/specify_cli/extensions/catalog/test_command_add.py ``` -Private phases use: +CLI-private phases use: ```text -src/specify_cli/extensions/_command_update_discovery.py -tests/specify_cli/extensions/test_command_update_discovery.py - -src/specify_cli/extensions/_command_update_artifacts.py -tests/specify_cli/extensions/test_command_update_artifacts.py - -src/specify_cli/extensions/_command_update_transaction.py -tests/specify_cli/extensions/test_command_update_transaction.py +src/specify_cli/_command_init_prompting.py +tests/specify_cli/test_command_init_prompting.py ``` The primary `test_command_.py` suite verifies the public command surface. Phase-specific suites verify detailed invariants without obscuring the primary command behavior. +Shared operation and phase tests use `test_operation_.py` and +`test_operation__.py` as defined in +[the shared testing structure](shared.md#testing-structure). They do not move +under `test_command_*.py` merely because the CLI is one caller. + Domain source remains in the parent package's `__init__.py` or a focused domain module without the `command_` prefix. Its mirrored tests use the domain subject name, for example: @@ -290,7 +309,8 @@ must remain represented. A matching total alone does not prove preservation. ## Reference layout -The extension command group currently demonstrates the complete pattern: +An extension command group using the shared application boundary has this +shape: ```text src/specify_cli/extensions/ @@ -305,9 +325,10 @@ src/specify_cli/extensions/ ├── command_search.py ├── command_set_priority.py ├── command_update.py -├── _command_update_discovery.py -├── _command_update_artifacts.py -├── _command_update_transaction.py +├── _operation_update.py +├── _operation_update_discovery.py +├── _operation_update_artifacts.py +├── _operation_update_transaction.py └── catalog/ ├── __init__.py ├── _helpers.py @@ -319,10 +340,11 @@ src/specify_cli/extensions/ The update command illustrates the distinction: - `command_update.py` is the registered CLI adapter. -- `_command_update_discovery.py` determines available updates. -- `_command_update_artifacts.py` prepares and validates update archives. -- `_command_update_transaction.py` owns backup, installation, rollback, and - cleanup behavior. +- `_operation_update.py` is the shared application entry point. +- `_operation_update_discovery.py` determines available updates. +- `_operation_update_artifacts.py` prepares and validates update archives. +- `_operation_update_transaction.py` owns backup, installation, rollback, and + cleanup behavior for every adapter. ## Decision guide @@ -331,9 +353,11 @@ When deciding where code belongs: | Question | Location | |---|---| | Does it define a real CLI command? | `command_.py` | -| Is it used only by one small command? | That command module | -| Is it a cohesive private phase of one complex command? | `_command__.py` | -| Is it shared by multiple commands or an external CLI flow? | `_commands.py` or a focused shared module | +| Is it a small CLI-only mapping or rendering helper? | That command module | +| Is it a cohesive CLI-only phase? | `_command__.py` | +| Does it define semantic validation or orchestration for an operation? | Existing domain module or `_operation_.py` | +| Is it a cohesive shared operation phase? | `_operation__.py` | +| Is it CLI infrastructure shared by multiple command adapters? | `_commands.py` or a focused CLI helper | | Does it define a nested CLI namespace? | A directory matching that namespace | | Is it shared only by commands in a nested namespace? | The nested package's `_helpers.py` | | Is it domain behavior independent of the CLI? | The package domain modules, not command modules | @@ -346,6 +370,10 @@ Avoid: - Naming a private implementation module `command_*.py`. - Creating nested directories that do not correspond to CLI namespaces. - Creating `_commands.py` files only for visual symmetry. +- Keeping semantic validation, orchestration, or side effects in a CLI adapter + when another adapter exposes the same logical operation. +- Calling or parsing another delivery adapter instead of invoking the shared + operation. - Moving command-private helpers into shared infrastructure preemptively. - Duplicating fixtures or helpers to make tests appear more mirrored. - Splitting a linear function into many files without cohesive phase @@ -359,9 +387,13 @@ For a new or refactored command: - [ ] The CLI path maps predictably to a `command_.py` module. - [ ] Only the real command module registers a handler. -- [ ] Private phase modules use `_command__.py`. +- [ ] The adapter maps into the shared operation defined by `design/shared.md`. +- [ ] Semantic validation, orchestration, and side effects are below the CLI + adapter. +- [ ] CLI-private phase modules use `_command__.py`; shared phases + use `_operation__.py`. - [ ] `_commands.py` contains only group infrastructure and genuinely shared - behavior. + CLI behavior. - [ ] Nested directories correspond to real CLI namespaces. - [ ] Command tests mirror the source structure. - [ ] Domain and cross-domain tests remain in their appropriate suites. diff --git a/design/mcp.md b/design/mcp.md index c180ccbb47..721fdf33cb 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -2,9 +2,13 @@ This document defines the architecture for exposing Specify operations through the Model Context Protocol (MCP). It is the MCP counterpart to -[Specify CLI Command Architecture](cli.md): command paths, ownership, -registration, contracts, tests, and operational policy should be predictable -from the surface being changed. +[Specify CLI Command Architecture](cli.md). + +Both adapters invoke the application layer defined by +[Shared Command Application Architecture](shared.md). This document owns MCP +tool identity, exposure, policy, protocol mapping, and transport. It does not +redefine semantic validation, orchestration, results, warnings, errors, or +side effects. ## Design goals @@ -45,55 +49,28 @@ This design does not: - Require an otherwise simple operation to be split into extra modules only for visual symmetry. -## One logical operation, two adapters - -For every MCP-eligible CLI leaf, the architecture has three conceptual -layers: - -```text -CLI arguments/options ─┐ - ├─> shared operation request -> application behavior -MCP tool input JSON ───┘ -> operation outcome - -operation outcome ─────┬─> CLI human or JSON rendering - └─> MCP structured content or tool error -``` - -The CLI and MCP adapters are peers: +## Shared operation dependency -- The CLI adapter owns Typer/Click declarations, terminal interaction, human - rendering, exit codes, and CLI JSON serialization. -- The MCP adapter owns tool metadata, MCP input/output schemas, protocol - result conversion, and MCP annotations. -- The shared operation owns semantic validation, orchestration, side effects, - typed results, warnings, and structured domain errors. +An MCP tool is a delivery adapter for a logical operation defined by +[the shared architecture](shared.md). It maps MCP input into the shared typed +request and maps the shared outcome into MCP structured content or a tool +error. -Neither adapter calls the other. In particular, the MCP adapter must not invoke -Typer handlers, start `specify` as a child process, scrape Rich output, or -parse CLI stderr. +The MCP adapter owns: -The shared operation is the behavioral source of truth. Adapters may differ -in presentation, but they must not differ in what the operation means. +- Tool name, description, annotations, and protocol schemas. +- Mapping between MCP content and shared request/outcome types. +- Per-group MCP registration and static inventory. +- Access-policy enforcement for MCP invocation. +- Protocol diagnostics and transport hosting. -## Ownership boundaries +It does not own semantic validation, application orchestration, side effects, +or command-specific domain contracts. It must not invoke Typer handlers, start +`specify` as application dispatch, scrape Rich output, or parse CLI stderr. -| Concern | Owner | -| --- | --- | -| Semantic request and result models | The relevant command/domain hierarchy | -| Semantic validation and orchestration | The shared operation/application module | -| Domain errors and warning codes | The relevant command/domain hierarchy | -| CLI arguments, prompts, text, JSON streams, and exit codes | `command_.py` | -| MCP tool name, description, annotations, and protocol conversion | `mcp_.py` | -| Per-group MCP registration and command inventory | The hierarchy's `_mcp.py` or small package registration module | -| Cross-command invocation context and access-policy primitives | Shared MCP/application infrastructure | -| MCP server lifecycle and transport hosting | `specify_cli/mcp_server/` | -| Authentication and connection concerns for future HTTP hosting | The HTTP transport layer | - -Command-specific contracts do not belong in a central MCP catalog. Shared MCP -infrastructure may define primitives such as `InvocationContext`, -`OperationWarning`, `OperationError`, access policy, cancellation, and output -budgets. It must not accumulate command-specific request models, result -models, validation, or orchestration. +Shared MCP infrastructure may define policy and protocol primitives. It must +not become a central catalog of command-specific request models, results, +validation, or orchestration. ## Operation identity and MCP tool design @@ -215,10 +192,8 @@ The conventions are: - `_mcp.py` owns explicit MCP registration and inventory for a command group. - Nested directories continue to correspond to real CLI namespaces or bounded subdomains, following [the CLI design](cli.md#nested-command-groups). -- The shared operation lives in the closest Typer-free domain module when one - already exists. -- If a dedicated application entry point is needed, use - `_operation_.py`. +- Shared operation and phase modules follow + [the shared naming rules](shared.md#naming-and-layout). - Tests mirror these names under `tests/specify_cli/`. Do not create a top-level MCP mirror of the entire CLI tree under @@ -231,65 +206,6 @@ register it through an existing focused registration module. Create `_mcp.py` when the hierarchy needs an explicit list of several tools, shared adapter helpers, or inventory dispositions. -## Simple and complex operations - -The same cohesion rules used for command-private phases apply below both -adapters. - -### Simple operation - -A simple operation may use an existing domain module or one focused operation -module: - -```text -command_version.py -mcp_version.py -_operation_version.py -``` - -Both adapters map to the typed operation in `_operation_version.py`. No extra -phase module is required. - -### Complex operation - -When a shared operation has cohesive phases with distinct invariants or -failure behavior, use: - -```text -_operation_.py -_operation__.py -``` - -For example: - -```text -command_init.py -mcp_init.py -_operation_init.py -_operation_init_validation.py -_operation_init_plan.py -_operation_init_apply.py -_operation_init_finalize.py -``` - -`_operation_init.py` remains the application entry point. Phase modules do not -register CLI commands or MCP tools. - -Adapter-only phases retain adapter-specific names: - -```text -_command__.py -_mcp__.py -``` - -Use them only when the phase genuinely belongs to presentation or protocol -adaptation. If both adapters need the phase, it belongs below them as an -operation or domain phase. - -Do not split a linear operation because it crossed an arbitrary line count. -Split when a phase has distinct invariants, rollback behavior, inputs, -outputs, or tests. - ## Registration and explicit inventory Registration remains explicit at each command-group boundary. @@ -373,155 +289,48 @@ declared and gated by capability. For example: Unavailability and exclusion are reviewable architecture decisions, not silent omissions. -## Command contracts - -### Typed requests - -The command/domain hierarchy owns a typed request model for semantic inputs. -The model: - -- Uses domain names rather than CLI flag spellings. -- Distinguishes omitted values from explicit false or empty values. -- Rejects unknown fields. -- Represents paths, enums, identifiers, and bounded collections explicitly. -- Contains explicit consent fields such as `force` or - `trust_extension_urls` only when the operation supports them. - -The CLI adapter maps parsed arguments and options into the request. The MCP -adapter exposes a command-specific JSON schema and maps schema-validated tool -input into the same request. - -Typer usage errors remain CLI concerns. Pure request errors and state-dependent -semantic errors belong to the operation so both adapters report the same -failure. - -### Validation and authorization order - -Capability authorization occurs before any state-dependent validation. The -invocation sequence is: - -1. The adapter parses and schema-validates transport input without filesystem, - network, environment, or process access. -2. The operation performs capability-free request validation. It may check - types, enum values, mutually exclusive fields, required combinations, and - other invariants derived solely from request values and static operation - metadata. -3. The operation computes `required_capabilities(request)` and request-specific - network requirements. This computation is pure and performs no I/O. -4. The access-policy layer authorizes every computed capability, network - requirement, and requested root. A denial stops the invocation. -5. Only after authorization may the operation resolve project state and run - state-dependent semantic validation, such as checking an integration, - reading project files, consulting a catalog, or inspecting an installed - tool. -6. The operation performs its side effects and returns its typed outcome. - -The capability computation must conservatively cover every path reachable -from the validated request. Stateful validation must not discover and then -exercise an additional unauthorized capability. If an invariant cannot be -checked without a capability, the check belongs after authorization. - -### Typed results and warnings - -The operation returns a typed outcome containing: - -- The command-specific result. -- Zero or more structured warnings. -- Execution metadata needed by adapters, such as changed paths or whether a - transaction committed. - -Warnings have a stable `code`, human-readable `message`, and optional typed -details. A warning is not printed inside the operation. The CLI adapter -renders it to the appropriate human or JSON channel; the MCP adapter includes -it in the command-specific structured result. - -Output types remain command-owned. There is no mandatory universal -`{"ok": true, "result": ...}` envelope. A shared outcome type is an internal -application mechanism, not a reason to replace established machine contracts. - -### Structured errors - -Expected failures use a typed operation error with: - -```text -code -message -details -retryable -``` - -The operation hierarchy owns the error code and details schema. The CLI -adapter maps it to human output or the command's JSON failure contract and -maps stable operation error codes to established CLI exit codes. The MCP -adapter maps it to an MCP tool error with structured content. Neither adapter -exposes a traceback, secret, raw subprocess output, or success-shaped -fallback. - -Unexpected exceptions are normalized at the adapter boundary to a sanitized -`internal_error`, logged only through the transport-appropriate diagnostic -channel. - -## Machine contracts and version metadata - -Existing CLI JSON contracts are compatibility constraints. Extracting a -shared operation must preserve field names, value semantics, stdout/stderr -purity, and error behavior unless a separately reviewed contract change says -otherwise. - -Each logical operation declares a `contract_version` in its inventory and MCP -tool metadata. The version identifies the request/result/warning/error -contract, not the MCP transport version or CLI package version. - -Contract version metadata must not be injected into an established result -whose schema does not already contain it. For example, the current -`specify version --json` result intentionally returns `cli_version`, -`runtime`, `system`, and `features` directly. Its MCP tool should preserve -that result shape while exposing the contract version through tool or -inventory metadata. - -Rules for contract evolution: - -- Backward-compatible optional fields and new warning codes may retain the - current major contract version. -- Removing, renaming, or changing the meaning of an input, output, warning, - or error requires a new major contract version and an explicit compatibility - strategy. -- Both adapters conform to the contract version declared by the - hierarchy-owned inventory. MCP exposes that version through tool or - inventory metadata; CLI contract tests reference the same declaration and - lock its machine result, warning, and error shapes without adding a new CLI - output field. -- An adapter-only presentation change does not change the operation contract - version. -- Tests lock established JSON shapes and MCP schemas at the command boundary. +## MCP contract projection + +Shared request, outcome, warning, error, validation, and contract-version rules +are defined by +[Shared Command Application Architecture](shared.md#typed-request-contract). +The MCP adapter projects that contract onto MCP: + +- Its input schema is command-specific and maps into the shared typed request. +- It performs protocol/schema validation but no state-dependent semantic work. +- It invokes the shared validation and authorization lifecycle before + application behavior. +- It maps the shared result and warnings into command-specific structured + content. +- It maps expected shared errors into MCP tool errors without adding + success-shaped fallbacks. +- It exposes the operation's declared `contract_version` through tool or + inventory metadata. + +MCP protocol envelopes do not force a universal application result envelope. +The command/domain hierarchy continues to own the semantic result shape. +Unexpected exceptions become sanitized `internal_error` tool failures and are +logged only through the MCP diagnostic channel. ## Invocation context and project resolution -Shared operations receive an immutable invocation context rather than reading -transport globals: - -```text -InvocationContext -├── launch_working_directory -├── project_root -├── allowed_roots -├── access_policy -├── deadline -├── cancellation -└── output_budget -``` +The MCP adapter constructs the immutable +[shared invocation context](shared.md#invocation-context) from server launch +state, host roots, active policy, and call lifecycle. Project-scoped MCP tools accept an optional project directory when their use case needs one. If omitted, project discovery starts from the server launch -working directory, matching normal CLI behavior. Resolution uses the same -domain helper as the CLI and produces the same semantic errors. +working directory, matching normal CLI behavior. The adapter carries that +requested context into the shared request; after capability authorization, the +shared operation resolves the project through the same domain helper used by +CLI and produces the same semantic errors. -The adapter resolves and normalizes paths before invoking the operation: +The invocation follows these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Pass the resolved project root explicitly through the operation and its - phases. +- Pass the requested directory and allowed roots explicitly; pass the resolved + project root through operation phases after shared resolution. - Enforce host-provided allowed roots when available. - Reject a path outside allowed roots with a structured policy error. - Do not infer the project from an unrelated server process state after the @@ -558,19 +367,10 @@ enforces operation requirements. ## Capability requirements and access policy -Every operation declares a set of independent capabilities. Policy must -authorize every capability required by the validated request; choosing one -"highest" class is not sufficient. - -| Capability | Meaning | Representative commands | -| --- | --- | --- | -| `local-read` | Reads process, host, or project state within allowed roots without persistent mutation | `version`, `artifact list` | -| `project-write` | Creates or changes files or configuration within an allowed project/target root | `init`, `extension add`, `preset enable` | -| `execution` | Starts host tools, workflows, hooks, agent/tool processes, or other executable behavior | `check`, `workflow run`, `workflow resume`, `event run` | -| `self-modifying` | Changes the Specify installation, server runtime, or machine-level state | `self upgrade` | - -Capabilities are cumulative requirements, not a hierarchy with implied -permissions. Examples: +Operations declare and compute capabilities according to +[the shared capability contract](shared.md#capability-declarations). MCP policy +must authorize every request-required capability; choosing one "highest" class +is not sufficient. Examples: ```text version -> {local-read} @@ -579,18 +379,6 @@ workflow.run -> {local-read, project-write, execution} self.upgrade -> {local-read, execution, self-modifying} ``` -The inventory declares the conservative set of capabilities any request for -the operation may require. When options activate materially different paths, -the command-owned operation may implement -`required_capabilities(request) -> set[Capability]` to compute the exact -subset after capability-free request validation. The computation uses only -request values and static operation metadata and must perform no filesystem, -network, environment, or process access. For example, `init` declares -`{local-read, project-write, execution}` because its normal tool checks launch -host binaries; a validated request that explicitly skips those checks may not -require `execution`. The MCP adapter and access-policy layer must not -independently infer or reduce the set. - `read-only` is a derived description, not an authorizable capability. A request is read-only only when it requires no `project-write`, `execution`, or `self-modifying` capability. An operation that launches a binary is therefore @@ -621,9 +409,8 @@ missing capabilities. A runtime describe surface may report An operation must not omit a capability merely because the path is rare, optional, expected to be idempotent, or combined with a more powerful -capability. The static declaration contains the union of possible -requirements; request-specific evaluation may only narrow it from validated -inputs. +capability. MCP enforces the shared operation's static and request-specific +declarations; it does not infer or reduce them independently. ## Trust, confirmation, and network responsibilities @@ -702,42 +489,9 @@ an architectural boundary, not a requirement to add empty files. ## Testing structure -Tests mirror source ownership: - -```text -src/specify_cli/artifacts/_operation_list.py -tests/specify_cli/artifacts/test_operation_list.py - -src/specify_cli/artifacts/command_list.py -tests/specify_cli/artifacts/test_command_list.py - -src/specify_cli/artifacts/mcp_list.py -tests/specify_cli/artifacts/test_mcp_list.py -``` - -Private operation phases use: - -```text -src/specify_cli/_operation_init_validation.py -tests/specify_cli/test_operation_init_validation.py -``` - -The required test layers are: - -### Operation tests - -- Cover valid requests and intended results. -- Cover invalid inputs, prevented behavior, and domain failures. -- Verify warnings, typed errors, side effects, rollback, cancellation, and - bounded behavior where applicable. -- Avoid Typer, MCP transport, and Rich assertions. - -### CLI adapter tests - -- Verify argument and option mapping. -- Verify prompts and non-interactive behavior. -- Verify human rendering, JSON streams, and exit codes. -- Preserve established help and compatibility import paths. +Shared operation, CLI adapter, and parity coverage follows +[the shared testing structure](shared.md#testing-structure) and +[the CLI test structure](cli.md#test-structure). MCP adds the following layers. ### MCP adapter tests @@ -747,22 +501,16 @@ The required test layers are: - Verify access-policy, trust, timeout, cancellation, and output-budget failures. -### Inventory and parity tests +### Inventory tests - Walk the actual CLI command tree and require one MCP inventory disposition for every leaf. - Reject duplicate operation IDs and MCP tool names. - Require reasons for every unavailable or excluded command. - Verify available tools are registered by the owning hierarchy. -- Run CLI JSON and MCP adapters against the same operation fixture and compare - semantic result, warning, error, and side-effect behavior. - Preserve total pytest collection when tests move, as required by the CLI architecture. -Adapter parity does not require byte-identical human terminal output. It -requires both adapters to invoke the same operation contract with equivalent -inputs and to represent the same outcome without inventing behavior. - ### Protocol tests - Keep an in-memory MCP registration and dispatch test. diff --git a/design/shared.md b/design/shared.md new file mode 100644 index 0000000000..431b84273b --- /dev/null +++ b/design/shared.md @@ -0,0 +1,395 @@ +# Shared Command Application Architecture + +This document defines the application layer beneath Specify delivery adapters. +A logical operation has one shared implementation. The CLI, MCP, and any future +delivery surface translate their own inputs into that operation and translate +its outcome into their own output and error conventions. + +[Specify CLI Command Architecture](cli.md) defines the Typer/terminal adapter. +[Specify MCP Command Architecture](mcp.md) defines MCP tool exposure, policy, +and transport. Neither adapter document owns application behavior. + +## Design goals + +The shared layer optimizes for: + +- **One behavior:** every adapter for a logical operation invokes the same + semantic implementation. +- **Adapter independence:** adapters own invocation and reporting without + calling or parsing one another. +- **Typed contracts:** requests, results, warnings, and expected errors are + explicit and testable. +- **Transport neutrality:** shared contracts contain no Typer, Rich, MCP, + stdout/stderr, protocol, or exit-code concerns. +- **Explicit context:** project roots, policy, deadlines, cancellation, and + output budgets are passed rather than read from mutable process globals. +- **Reviewable ownership:** application behavior has a predictable source and + mirrored tests. + +## Non-goals + +The shared layer does not: + +- Standardize how adapters spell arguments, display progress, or format human + output. +- Require byte-identical CLI JSON and MCP protocol envelopes. +- Turn the shared operation into a universal string-based command dispatcher. +- Replace focused domain modules with a central service locator. +- Make transport authentication, MCP authorization, or CLI prompting part of + domain behavior. +- Require extra modules for a small operation when an existing Typer-free + domain module is already the correct owner. + +## One logical operation, multiple adapters + +Adapters are peers above one application operation: + +```text +CLI arguments/options ─┐ +MCP tool input JSON ───┼─> typed operation request -> shared behavior +future adapter input ──┘ -> typed operation outcome + +typed operation outcome ─┬─> CLI text, JSON, warnings, and exit status + ├─> MCP structured content or tool error + └─> future adapter representation +``` + +The shared operation is the semantic source of truth. Adapters may expose +different presentation features, but equivalent requests must produce +equivalent results, warnings, expected failures, and side effects. + +An adapter must not: + +- Invoke another adapter. +- Parse another adapter's output. +- Reimplement application orchestration. +- Add semantic defaults, trust, consent, or side effects that are absent from + the shared request. + +## Ownership boundaries + +| Concern | Owner | +| --- | --- | +| Logical operation ID and contract version | Shared operation descriptor | +| Semantic request/result/warning/error models | Relevant command/domain hierarchy | +| Capability-free and state-dependent validation | Shared operation | +| Application orchestration and side effects | Shared operation and domain modules | +| Operation-private phases | `_operation__.py` | +| Invocation syntax and presentation | Delivery adapter | +| Human prompting and terminal rendering | CLI adapter | +| MCP tool schemas, annotations, and tool errors | MCP adapter | +| CLI exit-code mapping | CLI adapter | +| MCP access-policy enforcement and transport | MCP infrastructure | + +Domain behavior that already has a focused Typer-free owner may remain there. +A dedicated `_operation_.py` coordinates domain calls when the adapter +otherwise would own semantic validation or orchestration. + +## Logical operation identity + +The operation ID follows the CLI leaf path without the leading `specify`: + +```text +specify version -> version +specify artifact list -> artifact.list +specify extension set-priority -> extension.set-priority +``` + +The ID names application behavior, not a transport endpoint. Adapter identities +derive from it: + +```text +operation: artifact.list +CLI: specify artifact list +MCP: specify_artifact_list +``` + +An operation descriptor declares at least: + +```text +operation_id +contract_version +request_type +result_type +warning_types +error_types +capabilities +network_access +project_scope +default_timeout +``` + +Adapter-specific registration metadata extends this descriptor without moving +the shared fields into adapter infrastructure. + +## Naming and layout + +When a focused shared application entry point is required, use: + +```text +_operation_.py +``` + +For example: + +```text +src/specify_cli/ +├── _operation_version.py +├── command_version.py +└── mcp_version.py +``` + +A simple operation stays in one operation or existing domain module. Do not +split it for symmetry. + +When a complex operation has cohesive phases with distinct invariants, failure +behavior, rollback, or tests, use: + +```text +_operation_.py +_operation__.py +``` + +For example: + +```text +src/specify_cli/ +├── _operation_init.py +├── _operation_init_validation.py +├── _operation_init_plan.py +├── _operation_init_apply.py +├── _operation_init_finalize.py +├── command_init.py +└── mcp_init.py +``` + +`_operation_init.py` is the application entry point. Phase modules do not +register commands or tools. + +Adapter-only phases retain adapter-specific names: + +```text +_command__.py +_mcp__.py +``` + +If more than one adapter needs a phase, it is not adapter-private and belongs +in the shared operation or domain layer. + +Nested directories continue to represent real command namespaces or bounded +subdomains. Do not create an operation-phase directory that implies a +nonexistent CLI namespace. + +## Typed request contract + +The relevant command/domain hierarchy owns a typed request model. It: + +- Uses semantic names rather than CLI flag or MCP field implementation names. +- Distinguishes omitted values from explicit false, empty, or null values. +- Rejects unknown fields. +- Represents paths, enums, identifiers, and bounded collections explicitly. +- Carries explicit consent such as `force` or external-source trust only when + the operation defines that behavior. + +Adapters perform transport parsing and map into this request. They do not +perform state-dependent semantic work while constructing it. + +## Validation and authorization lifecycle + +No denied capability may be exercised while deciding whether a request is +authorized. Invocation follows this order: + +1. The adapter parses and schema-validates input without filesystem, network, + environment, or process access. +2. The shared operation performs capability-free request validation using + only request values and static operation metadata. +3. The shared operation computes request-required capabilities, network + requirements, and requested roots without I/O. +4. The applicable policy layer authorizes those requirements. A denial stops + the invocation. +5. Under authorized capabilities, the shared operation resolves project + context and performs state-dependent validation. +6. The shared operation performs side effects and returns its typed outcome. + +Capability-free validation covers types, enums, mutually exclusive fields, +required combinations, and similar pure invariants. State-dependent +validation includes reading project files, resolving installed integrations, +consulting catalogs, inspecting host tools, and other I/O. + +The computed requirements must conservatively cover every path reachable from +the pure validated request. Stateful validation must not discover and exercise +an additional unauthorized capability. + +## Typed outcome contract + +The operation returns a typed outcome containing: + +- The command-specific result. +- Zero or more structured warnings. +- Adapter-relevant execution metadata such as changed paths or transaction + status. + +Warnings have a stable code, human-readable message, and typed details. The +operation does not print them. Each adapter decides how its surface represents +them. + +There is no mandatory universal success envelope. Command-specific output +types remain owned by the command/domain hierarchy. + +## Structured errors + +Expected failures use a transport-neutral operation error: + +```text +code +message +details +retryable +``` + +The operation hierarchy owns error codes and detail schemas. Adapters map them: + +- CLI maps them to human or JSON failure output and established exit codes. +- MCP maps them to structured tool errors. + +Shared errors contain no CLI exit code, Rich markup, MCP content block, HTTP +status, traceback, raw subprocess output, or secret. + +Unexpected exceptions are normalized by the adapter boundary to a sanitized +internal error and logged only through the adapter's diagnostic channel. + +## Contract versions and machine compatibility + +The operation descriptor is the source of truth for `contract_version`. The +version covers the semantic request, result, warning, and expected-error +contract, not package or transport versions. + +Both adapters conform to that declared version: + +- MCP exposes it through tool or inventory metadata. +- CLI contract tests reference it while preserving established human and JSON + output. A version field is not injected into an existing JSON result unless + that result contract already defines one. + +Contract evolution follows these rules: + +- Backward-compatible optional fields and warning codes may retain the current + major version. +- Removing, renaming, or changing the meaning of an input, output, warning, or + error requires a new major version and explicit compatibility strategy. +- Adapter-only presentation changes do not change the operation contract + version. +- Tests lock established adapter schemas and machine-output shapes to the + declared contract. + +## Invocation context + +Shared operations receive an immutable context rather than reading adapter or +process globals: + +```text +InvocationContext +├── launch_working_directory +├── project_root +├── allowed_roots +├── access_policy +├── deadline +├── cancellation +└── output_budget +``` + +Not every operation uses every field. Adapters and application infrastructure +construct the context; command/domain code consumes it explicitly. + +Shared operations must not call `os.chdir()` to establish request context. +They pass resolved roots through operation phases and domain calls. Deadlines, +cancellation, and output budgets are likewise explicit. + +## Capability declarations + +Capabilities are independent requirements, not a highest-risk hierarchy: + +| Capability | Meaning | +| --- | --- | +| `local-read` | Read process, host, or project state within allowed roots | +| `project-write` | Create or change project/target files or configuration | +| `execution` | Start host tools, workflows, hooks, agents, or processes | +| `self-modifying` | Change the Specify installation or machine-level state | + +The descriptor declares the conservative union an operation may require. +`required_capabilities(request)` may compute an exact subset only from the +capability-free validated request and static metadata. + +Network access is declared separately as `none`, `optional`, or `required`. +Trust and destructive consent remain explicit request values, not implied +capabilities. + +The MCP design defines policy enforcement for its tool surface. CLI invocation +and reporting remain defined by the CLI design, but the CLI adapter must not +change the operation's capability, trust, or consent semantics. + +## Testing structure + +Tests mirror source ownership: + +```text +src/specify_cli/artifacts/_operation_list.py +tests/specify_cli/artifacts/test_operation_list.py + +src/specify_cli/artifacts/command_list.py +tests/specify_cli/artifacts/test_command_list.py + +src/specify_cli/artifacts/mcp_list.py +tests/specify_cli/artifacts/test_mcp_list.py +``` + +Operation tests cover: + +- Valid requests and intended results. +- Pure and state-dependent validation failures. +- Warnings and structured errors. +- Capabilities, trust, consent, network behavior, side effects, rollback, + cancellation, and output bounds. +- Domain behavior without Typer, Rich, MCP, or transport assertions. + +Adapter tests cover invocation mapping and adapter-specific reporting. + +Parity tests invoke CLI and MCP adapters against the same operation fixture +and compare semantic request, result, warning, error, and side-effect behavior. +Parity does not require byte-identical presentation. + +Behavioral changes follow +[Testing deterministic behavior](../CONTRIBUTING.md#testing-deterministic-behavior): +positive and negative evidence is required, and bug fixes need before-and-after +regression evidence. + +## Anti-patterns + +Avoid: + +- Calling a Typer handler from MCP or an MCP tool from CLI. +- Invoking the human CLI as application dispatch. +- Parsing Rich, stdout, stderr, or protocol output to recover domain results. +- Duplicating validation or orchestration in adapters. +- Adding adapter concepts to request, outcome, warning, or error models. +- Hiding operations behind a central string dispatcher or service locator. +- Letting adapters infer force, trust, consent, or extra capabilities. +- Reading mutable process cwd instead of using invocation context. +- Splitting simple operations or creating phase modules solely for symmetry. + +## Review checklist + +For an operation with CLI and MCP adapters: + +- [ ] One logical operation ID identifies both surfaces. +- [ ] Both adapters map into the same typed request and shared entry point. +- [ ] Semantic validation, orchestration, and side effects are below adapters. +- [ ] Adapter modules contain only invocation, mapping, presentation, and + adapter-specific concerns. +- [ ] Shared request, outcome, warning, and error types are transport-neutral. +- [ ] Capability computation is pure and authorization precedes stateful work. +- [ ] CLI exit codes and MCP tool errors remain adapter-owned. +- [ ] Contract-version ownership and compatibility tests are explicit. +- [ ] Operation tests and adapter parity tests cover positive and negative + behavior. +- [ ] No adapter invokes or parses another adapter. From db1317868c7ead29ac98f8e4cdcf017e57233025 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:30:56 -0500 Subject: [PATCH 07/19] docs: authorize before project resolution Separate the I/O-free pre-authorization context from the canonical authorized operation context and require a post-resolution allowed-root check. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 30 +++++++++++++++----------- design/shared.md | 56 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index 721fdf33cb..53e560de7b 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -315,8 +315,10 @@ logged only through the MCP diagnostic channel. ## Invocation context and project resolution The MCP adapter constructs the immutable -[shared invocation context](shared.md#invocation-context) from server launch -state, host roots, active policy, and call lifecycle. +[shared pre-authorization context](shared.md#invocation-context) from the +requested directory, server launch state, host roots, active policy, and call +lifecycle. It performs no project discovery or filesystem resolution while +constructing that context. Project-scoped MCP tools accept an optional project directory when their use case needs one. If omitted, project discovery starts from the server launch @@ -329,10 +331,13 @@ The invocation follows these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Pass the requested directory and allowed roots explicitly; pass the resolved - project root through operation phases after shared resolution. -- Enforce host-provided allowed roots when available. -- Reject a path outside allowed roots with a structured policy error. +- Pass the unresolved requested directory and host-provided allowed roots + explicitly. +- Perform preliminary policy checks without filesystem access, then resolve + the canonical project root under authorized `local-read`. +- Re-check canonical allowed-root containment after resolution and reject + escapes with a structured policy error. +- Pass only the authorized resolved project root through operation phases. - Do not infer the project from an unrelated server process state after the invocation begins. @@ -585,12 +590,13 @@ network_access: none project_scope: required ``` -The request contains an optional project directory. Project resolution -produces a normalized root in the invocation context. `_operation_list.py` -uses `ArtifactCatalog` and returns typed artifact rows. The CLI adapter -preserves its JSON stream contract; the MCP adapter exposes the rows through -its output schema and never captures CLI stdout. Invocation output budgets -must produce explicit bounded-output behavior rather than silent truncation. +The request contains an optional project directory. After authorization, +project resolution produces a canonical root in the authorized operation +context and re-checks allowed-root containment. `_operation_list.py` uses +`ArtifactCatalog` and returns typed artifact rows. The CLI adapter preserves +its JSON stream contract; the MCP adapter exposes the rows through its output +schema and never captures CLI stdout. Invocation output budgets must produce +explicit bounded-output behavior rather than silent truncation. ### `init`: complex project mutation diff --git a/design/shared.md b/design/shared.md index 431b84273b..387a74bb7a 100644 --- a/design/shared.md +++ b/design/shared.md @@ -21,8 +21,9 @@ The shared layer optimizes for: explicit and testable. - **Transport neutrality:** shared contracts contain no Typer, Rich, MCP, stdout/stderr, protocol, or exit-code concerns. -- **Explicit context:** project roots, policy, deadlines, cancellation, and - output budgets are passed rather than read from mutable process globals. +- **Explicit context:** requested directories, authorized project roots, + policy, deadlines, cancellation, and output budgets are passed rather than + read from mutable process globals. - **Reviewable ownership:** application behavior has a predictable source and mirrored tests. @@ -205,10 +206,12 @@ authorized. Invocation follows this order: only request values and static operation metadata. 3. The shared operation computes request-required capabilities, network requirements, and requested roots without I/O. -4. The applicable policy layer authorizes those requirements. A denial stops - the invocation. -5. Under authorized capabilities, the shared operation resolves project - context and performs state-dependent validation. +4. The applicable policy layer authorizes those requirements and performs a + preliminary allowed-root check on the unresolved requested path. A denial + stops the invocation. +5. Under authorized `local-read`, the shared operation resolves the canonical + project or target root, re-checks allowed-root containment, and performs + state-dependent validation. 6. The shared operation performs side effects and returns its typed outcome. Capability-free validation covers types, enums, mutually exclusive fields, @@ -217,8 +220,11 @@ validation includes reading project files, resolving installed integrations, consulting catalogs, inspecting host tools, and other I/O. The computed requirements must conservatively cover every path reachable from -the pure validated request. Stateful validation must not discover and exercise -an additional unauthorized capability. +the pure validated request. Canonical resolution can inspect the filesystem +and follow symlinks, so it must not occur before authorization. The +post-resolution containment check prevents an unresolved path that appeared +allowed from escaping through a symlink. Stateful validation must not discover +and exercise an additional unauthorized capability. ## Typed outcome contract @@ -284,13 +290,12 @@ Contract evolution follows these rules: ## Invocation context -Shared operations receive an immutable context rather than reading adapter or -process globals: +Adapters begin with an immutable, I/O-free pre-authorization context: ```text -InvocationContext +PreAuthorizationContext ├── launch_working_directory -├── project_root +├── requested_directory ├── allowed_roots ├── access_policy ├── deadline @@ -298,8 +303,24 @@ InvocationContext └── output_budget ``` -Not every operation uses every field. Adapters and application infrastructure -construct the context; command/domain code consumes it explicitly. +`requested_directory` is the caller-supplied value, or absent when discovery +should begin from `launch_working_directory`. Constructing this context must +not read the environment, inspect the filesystem, resolve symlinks, or discover +a project. + +After policy authorization, shared application infrastructure resolves and +validates the canonical root and constructs: + +```text +AuthorizedOperationContext +├── invocation: PreAuthorizationContext +└── project_root +``` + +`project_root` is optional for process-scoped operations and represents the +canonical project or target root for scoped operations. Canonical containment +against `allowed_roots` is checked again before state-dependent validation or +side effects. Shared operations must not call `os.chdir()` to establish request context. They pass resolved roots through operation phases and domain calls. Deadlines, @@ -320,6 +341,9 @@ The descriptor declares the conservative union an operation may require. `required_capabilities(request)` may compute an exact subset only from the capability-free validated request and static metadata. +Resolving or validating a project or target root requires `local-read`, even +when the operation's eventual side effect is `project-write`. + Network access is declared separately as `none`, `optional`, or `required`. Trust and destructive consent remain explicit request values, not implied capabilities. @@ -350,6 +374,8 @@ Operation tests cover: - Warnings and structured errors. - Capabilities, trust, consent, network behavior, side effects, rollback, cancellation, and output bounds. +- Preliminary requested-root authorization and canonical post-resolution + containment, including symlink-escape rejection. - Domain behavior without Typer, Rich, MCP, or transport assertions. Adapter tests cover invocation mapping and adapter-specific reporting. @@ -388,6 +414,8 @@ For an operation with CLI and MCP adapters: adapter-specific concerns. - [ ] Shared request, outcome, warning, and error types are transport-neutral. - [ ] Capability computation is pure and authorization precedes stateful work. +- [ ] Canonical project resolution and its allowed-root re-check occur only + after authorization. - [ ] CLI exit codes and MCP tool errors remain adapter-owned. - [ ] Contract-version ownership and compatibility tests are explicit. - [ ] Operation tests and adapter parity tests cover positive and negative From f244cf5914eea1f48e3c16dc53e0f8c0b4b70489 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:39:49 -0500 Subject: [PATCH 08/19] docs: define enforceable MCP confinement Require root-bound filesystem access at every operation and distinguish sandboxed execution from explicit unrestricted host execution, while correcting CLI private-phase examples. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 7 +++-- design/mcp.md | 60 ++++++++++++++++++++++++++++++++-------- design/shared.md | 72 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 118 insertions(+), 21 deletions(-) diff --git a/design/cli.md b/design/cli.md index 0b88b6b971..82c407b6e2 100644 --- a/design/cli.md +++ b/design/cli.md @@ -90,7 +90,7 @@ _command_init_rendering.py ``` The leading underscore marks the module as private implementation. The -`command_update` portion groups it with the registered handler in searches and +`command_init` portion groups it with the registered handler in searches and file listings. The phase suffix communicates its ownership. Private phase modules must not register additional CLI commands. The public @@ -198,8 +198,9 @@ re-exports the command symbols for compatibility. Do not create a nested directory for an implementation phase that is not a CLI subcommand. For example, an `update/` directory would incorrectly suggest an -`extension update ...` subcommand group. Use `_command_update_.py` -instead. +`extension update ...` subcommand group. Use `_command_update_.py` for +a CLI-private phase and `_operation_update_.py` for semantic validation, +planning, mutation, rollback, or other behavior shared with another adapter. ## Registration diff --git a/design/mcp.md b/design/mcp.md index 53e560de7b..091029573c 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -335,9 +335,14 @@ The invocation follows these rules: explicitly. - Perform preliminary policy checks without filesystem access, then resolve the canonical project root under authorized `local-read`. -- Re-check canonical allowed-root containment after resolution and reject - escapes with a structured policy error. -- Pass only the authorized resolved project root through operation phases. +- Re-check canonical allowed-root containment after resolution as an admission + check. +- Pass the authorized root and root-bound filesystem interface through + operation phases. +- Enforce containment at every descendant access with descriptor-/handle-based + no-follow traversal or an equivalent fail-closed platform mechanism; do not + rely on the one root check to prevent later symlink, junction, or path-swap + escapes. - Do not infer the project from an unrelated server process state after the invocation begins. @@ -358,13 +363,15 @@ MCP operations are always non-interactive: `confirmation_required` error explaining which field must be supplied. Machine mode is not consent. An MCP call, `--json`, `--non-interactive`, a -host confirmation dialog, or an authorized capability set does not imply: +host confirmation dialog, or authorization of `local-read`, `project-write`, +or `execution` does not imply: - `force=true`. - Trust of an external URL or downloaded executable content. - Permission to overwrite user-modified files. - Permission to leave the declared project root. - Permission to execute a workflow, hook, installer, or arbitrary command. +- Permission for a child process to use unrestricted host access. Consent must be explicit in the command request and valid under the active access policy. MCP annotations and host UI are advisory; the server still @@ -379,11 +386,18 @@ is not sufficient. Examples: ```text version -> {local-read} -check -> {local-read, execution} -workflow.run -> {local-read, project-write, execution} -self.upgrade -> {local-read, execution, self-modifying} +check -> {local-read, execution, unrestricted-host-execution} +workflow.run -> {local-read, project-write, execution, + unrestricted-host-execution} +self.upgrade -> {local-read, execution, unrestricted-host-execution, + self-modifying} ``` +These examples conservatively classify their child processes as using ambient +host privileges. An implementation may omit `unrestricted-host-execution` only +when an enforceable sandbox contains the child within its authorized +filesystem, network, environment, and subprocess boundaries. + `read-only` is a derived description, not an authorizable capability. A request is read-only only when it requires no `project-write`, `execution`, or `self-modifying` capability. An operation that launches a binary is therefore @@ -399,9 +413,14 @@ default policy is conservative: - `local-read` is authorized. - `project-write`, `execution`, and `self-modifying` require explicit authorization. -- Filesystem access is limited to host-provided roots or, when none are - provided, the server launch working directory. -- Network access is denied unless explicitly enabled. +- `unrestricted-host-execution` requires a separate explicit authorization and + remains default-deny. +- In-process filesystem access is limited to host-provided roots or, when none + are provided, the server launch working directory, with the boundary enforced + at each access. +- Server-managed and sandboxed network access is denied unless explicitly + enabled. An authorized unrestricted host child is a disclosed broad + exception, not a network-confined execution mode. - External-source trust is separately controlled and remains default-deny. Tool annotations should conservatively reflect the full declared capability @@ -417,6 +436,13 @@ optional, expected to be idempotent, or combined with a more powerful capability. MCP enforces the shared operation's static and request-specific declarations; it does not infer or reduce them independently. +An MCP execution request must use a host sandbox that enforces its authorized +filesystem, network, environment, and subprocess boundaries. If the +implementation launches a child with ambient server-user access instead, the +operation descriptor and request-specific capability calculation must require +`unrestricted-host-execution`. The server must deny the request when that +separate grant is absent; setting cwd inside the project is not confinement. + ## Trust, confirmation, and network responsibilities The shared operation owns the semantic rule that an action requires trust or @@ -505,6 +531,9 @@ Shared operation, CLI adapter, and parity coverage follows - Verify structured warnings and tool errors. - Verify access-policy, trust, timeout, cancellation, and output-budget failures. +- Verify descendant filesystem escapes are rejected at the point of access. +- Verify unsandboxed child execution requires + `unrestricted-host-execution` and never occurs as a fallback. ### Inventory tests @@ -592,7 +621,8 @@ project_scope: required The request contains an optional project directory. After authorization, project resolution produces a canonical root in the authorized operation -context and re-checks allowed-root containment. `_operation_list.py` uses +context, re-checks allowed-root containment, and uses the root-bound filesystem +interface for every descendant access. `_operation_list.py` uses `ArtifactCatalog` and returns typed artifact rows. The CLI adapter preserves its JSON stream contract; the MCP adapter exposes the rows through its output schema and never captures CLI stdout. Invocation output budgets must produce @@ -629,7 +659,8 @@ Contract: operation_id: init cli_path: specify init mcp_tool_name: specify_init -capabilities: [local-read, project-write, execution] +capabilities: [local-read, project-write, execution, + unrestricted-host-execution] network_access: optional project_scope: creates-target ``` @@ -641,6 +672,11 @@ shared operation validates inputs, builds a plan, applies transactional changes, and returns created/updated paths plus structured warnings. The CLI adapter may gather interactive choices before constructing the same request. +`init` tool checks launch host binaries, so this example conservatively +requires `unrestricted-host-execution`. A sandboxed implementation may omit +that capability, but it must not infer the grant from `execution` or silently +fall back when a sandbox is unavailable. + If the target is non-empty and `force` is false, both adapters receive the same semantic confirmation-required failure. The CLI may respond by prompting and retrying with explicit consent; the MCP tool returns the structured error and diff --git a/design/shared.md b/design/shared.md index 387a74bb7a..cf8cee354e 100644 --- a/design/shared.md +++ b/design/shared.md @@ -212,7 +212,10 @@ authorized. Invocation follows this order: 5. Under authorized `local-read`, the shared operation resolves the canonical project or target root, re-checks allowed-root containment, and performs state-dependent validation. -6. The shared operation performs side effects and returns its typed outcome. +6. The shared operation performs every filesystem access through the + authorized root-bound filesystem boundary. +7. The shared operation performs its side effects and returns its typed + outcome. Capability-free validation covers types, enums, mutually exclusive fields, required combinations, and similar pure invariants. State-dependent @@ -222,9 +225,9 @@ consulting catalogs, inspecting host tools, and other I/O. The computed requirements must conservatively cover every path reachable from the pure validated request. Canonical resolution can inspect the filesystem and follow symlinks, so it must not occur before authorization. The -post-resolution containment check prevents an unresolved path that appeared -allowed from escaping through a symlink. Stateful validation must not discover -and exercise an additional unauthorized capability. +post-resolution containment check is an admission check, not continuing proof +of confinement. Stateful validation must not discover and exercise an +additional unauthorized capability. ## Typed outcome contract @@ -314,18 +317,46 @@ validates the canonical root and constructs: ```text AuthorizedOperationContext ├── invocation: PreAuthorizationContext -└── project_root +├── project_root +└── filesystem: RootedFilesystem ``` `project_root` is optional for process-scoped operations and represents the canonical project or target root for scoped operations. Canonical containment against `allowed_roots` is checked again before state-dependent validation or -side effects. +side effects. `RootedFilesystem` is the policy-enforcing interface for +subsequent file access; a raw canonical path is identity, not authorization. Shared operations must not call `os.chdir()` to establish request context. They pass resolved roots through operation phases and domain calls. Deadlines, cancellation, and output budgets are likewise explicit. +## Filesystem confinement + +Root validation alone does not confine later access. A descendant can be a +symlink or junction to an outside path, and a path component can be replaced +between validation and use. + +Every filesystem access made under a confined policy must therefore use +`RootedFilesystem` or an operation-owned equivalent that enforces the same +invariants: + +- Anchor traversal at an already-authorized root. +- Validate each descendant component without following unauthorized symlinks, + junctions, mount redirections, or equivalent platform indirections. +- Couple validation and use through descriptor-relative or handle-relative + access where the platform supports it. +- Re-check containment at the point of access when handle-relative traversal + is unavailable, reject indirections before and after creation, and fail + closed when the platform cannot enforce the boundary safely. +- Apply the boundary to reads, writes, creates, deletes, renames, temporary + files, archives, caches, and rollback paths. + +Command/domain code must not bypass the boundary with raw `Path`, `open`, or +unscoped filesystem helpers. Shared infrastructure may provide these +root-bound primitives, but command-specific path semantics remain owned by the +relevant hierarchy. + ## Capability declarations Capabilities are independent requirements, not a highest-risk hierarchy: @@ -335,6 +366,7 @@ Capabilities are independent requirements, not a highest-risk hierarchy: | `local-read` | Read process, host, or project state within allowed roots | | `project-write` | Create or change project/target files or configuration | | `execution` | Start host tools, workflows, hooks, agents, or processes | +| `unrestricted-host-execution` | Permit an executed child to use the server user's ambient host access outside allowed roots | | `self-modifying` | Change the Specify installation or machine-level state | The descriptor declares the conservative union an operation may require. @@ -344,6 +376,26 @@ capability-free validated request and static metadata. Resolving or validating a project or target root requires `local-read`, even when the operation's eventual side effect is `project-write`. +`execution` does not waive filesystem confinement. An operation that starts a +child process must either: + +- Run it inside an enforceable sandbox limited to authorized roots, network, + environment, and subprocess behavior; or +- Declare `unrestricted-host-execution` in addition to `execution`. + +The second capability is an explicit acknowledgement that the child runs with +the server user's ambient privileges and can access paths outside +`allowed_roots`. It is never implied by `execution`, project cwd, machine mode, +or transport authentication. If sandboxing is required but unavailable, the +operation fails with a structured policy error; it must not silently fall back +to unrestricted execution. + +When `unrestricted-host-execution` is authorized, filesystem, environment, +network, and subprocess restrictions cannot be claimed as enforced inside the +child. Policy must present the grant as that broad exception. Operation +descriptors still declare their intended managed network behavior, but an +arbitrary unsandboxed child is not a network-confined execution mode. + Network access is declared separately as `none`, `optional`, or `required`. Trust and destructive consent remain explicit request values, not implied capabilities. @@ -376,6 +428,10 @@ Operation tests cover: cancellation, and output bounds. - Preliminary requested-root authorization and canonical post-resolution containment, including symlink-escape rejection. +- Root-bound enforcement at each descendant filesystem access, including + symlink/junction replacement and time-of-check/time-of-use cases. +- Sandboxed execution and explicit `unrestricted-host-execution` policy + denial, with no unsafe fallback. - Domain behavior without Typer, Rich, MCP, or transport assertions. Adapter tests cover invocation mapping and adapter-specific reporting. @@ -416,6 +472,10 @@ For an operation with CLI and MCP adapters: - [ ] Capability computation is pure and authorization precedes stateful work. - [ ] Canonical project resolution and its allowed-root re-check occur only after authorization. +- [ ] Every confined filesystem access remains anchored to authorized roots at + the point of use. +- [ ] Child processes are sandboxed or require explicit + `unrestricted-host-execution`. - [ ] CLI exit codes and MCP tool errors remain adapter-owned. - [ ] Contract-version ownership and compatibility tests are explicit. - [ ] Operation tests and adapter parity tests cover positive and negative From 2cc7b02c743e5b40859f98c34a7ea276fa5ef4fa Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:55:07 -0500 Subject: [PATCH 09/19] docs: define direct CLI access policy Make filesystem scope and capability policy explicit for the direct CLI while preserving root-bound MCP behavior and policy-aware adapter parity. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 39 ++++++++++++++++++++++++ design/mcp.md | 8 ++--- design/shared.md | 79 +++++++++++++++++++++++++++++++----------------- 3 files changed, 94 insertions(+), 32 deletions(-) diff --git a/design/cli.md b/design/cli.md index 82c407b6e2..ac4301d02e 100644 --- a/design/cli.md +++ b/design/cli.md @@ -221,6 +221,39 @@ Registration imports should be explicit and ordered consistently. Do not rely on filesystem discovery to import arbitrary modules, because command exposure should remain reviewable in one place. +## Shared invocation context and direct CLI policy + +The direct local CLI constructs the +[shared pre-authorization context](shared.md#invocation-context) explicitly: + +- `launch_working_directory` is captured once when the CLI invocation begins. +- `requested_directory` comes from the command's typed input, or remains absent + when project discovery should begin from the launch directory. +- `filesystem_scope` is `HostUser`, preserving established CLI behavior for + explicit paths such as an `init` target outside the launch directory. +- `access_policy` is `DirectCliPolicy`. +- Deadline, cancellation, and output-budget values come from CLI invocation + infrastructure rather than mutable command globals. + +`DirectCliPolicy` represents a user intentionally running a local command under +that user's operating-system permissions. It authorizes the capabilities +declared for the selected operation, including ambient host execution when the +descriptor requires `unrestricted-host-execution`. The shared operation does +not infer this policy and must not substitute MCP's launch-directory boundary. + +This policy preserves host access, not semantic consent: + +- `--json`, `--non-interactive`, and local invocation do not imply `force`. +- External-source trust and overwrite consent remain explicit request values. +- Command-specific prompts may collect consent before constructing a new + request, but the adapter does not add consent silently. +- Network defaults and self-modifying behavior remain those explicitly defined + by the selected operation and its CLI contract. + +An embedded CLI host may supply a `RootBound` scope and a stricter access policy +instead. Adapter parity compares equivalent requests under equivalent policy +contexts; a policy denial is not an application-behavior divergence. + ## Test structure Command-focused tests mirror the source command surface under @@ -247,6 +280,10 @@ The primary `test_command_.py` suite verifies the public command surface. Phase-specific suites verify detailed invariants without obscuring the primary command behavior. +CLI adapter tests also verify `DirectCliPolicy` construction, including +explicit targets outside the launch directory and the rule that machine modes +do not add force, trust, or destructive consent. + Shared operation and phase tests use `test_operation_.py` and `test_operation__.py` as defined in [the shared testing structure](shared.md#testing-structure). They do not move @@ -391,6 +428,8 @@ For a new or refactored command: - [ ] The adapter maps into the shared operation defined by `design/shared.md`. - [ ] Semantic validation, orchestration, and side effects are below the CLI adapter. +- [ ] Direct CLI invocation constructs `HostUser` filesystem scope and + `DirectCliPolicy`; embedded confinement is explicit. - [ ] CLI-private phase modules use `_command__.py`; shared phases use `_operation__.py`. - [ ] `_commands.py` contains only group infrastructure and genuinely shared diff --git a/design/mcp.md b/design/mcp.md index 091029573c..6e3c13900a 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -317,8 +317,8 @@ logged only through the MCP diagnostic channel. The MCP adapter constructs the immutable [shared pre-authorization context](shared.md#invocation-context) from the requested directory, server launch state, host roots, active policy, and call -lifecycle. It performs no project discovery or filesystem resolution while -constructing that context. +lifecycle. Its filesystem scope is `RootBound(allowed_roots)`. It performs no +project discovery or filesystem resolution while constructing that context. Project-scoped MCP tools accept an optional project directory when their use case needs one. If omitted, project discovery starts from the server launch @@ -331,8 +331,8 @@ The invocation follows these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Pass the unresolved requested directory and host-provided allowed roots - explicitly. +- Pass the unresolved requested directory and + `RootBound(host_provided_allowed_roots)` explicitly. - Perform preliminary policy checks without filesystem access, then resolve the canonical project root under authorized `local-read`. - Re-check canonical allowed-root containment after resolution as an admission diff --git a/design/shared.md b/design/shared.md index cf8cee354e..8b98c4a0da 100644 --- a/design/shared.md +++ b/design/shared.md @@ -56,8 +56,9 @@ typed operation outcome ─┬─> CLI text, JSON, warnings, and exit status ``` The shared operation is the semantic source of truth. Adapters may expose -different presentation features, but equivalent requests must produce -equivalent results, warnings, expected failures, and side effects. +different presentation features, but equivalent requests under equivalent +authorized contexts must produce equivalent results, warnings, expected +failures, and side effects. An adapter must not: @@ -206,14 +207,14 @@ authorized. Invocation follows this order: only request values and static operation metadata. 3. The shared operation computes request-required capabilities, network requirements, and requested roots without I/O. -4. The applicable policy layer authorizes those requirements and performs a - preliminary allowed-root check on the unresolved requested path. A denial - stops the invocation. +4. The applicable policy layer authorizes those requirements. Under a + root-bound filesystem scope, it also performs a preliminary allowed-root + check on the unresolved requested path. A denial stops the invocation. 5. Under authorized `local-read`, the shared operation resolves the canonical - project or target root, re-checks allowed-root containment, and performs + project or target root. A root-bound scope re-checks containment before state-dependent validation. 6. The shared operation performs every filesystem access through the - authorized root-bound filesystem boundary. + authorized filesystem interface. 7. The shared operation performs its side effects and returns its typed outcome. @@ -299,7 +300,7 @@ Adapters begin with an immutable, I/O-free pre-authorization context: PreAuthorizationContext ├── launch_working_directory ├── requested_directory -├── allowed_roots +├── filesystem_scope ├── access_policy ├── deadline ├── cancellation @@ -311,6 +312,18 @@ should begin from `launch_working_directory`. Constructing this context must not read the environment, inspect the filesystem, resolve symlinks, or discover a project. +`filesystem_scope` is explicit: + +```text +RootBound(allowed_roots) +HostUser +``` + +`RootBound` confines access to host-provided roots. `HostUser` preserves the +direct local CLI model: filesystem access is governed by the invoking user's +operating-system permissions rather than an application root boundary. An +adapter must choose one; absence of a scope never means unrestricted access. + After policy authorization, shared application infrastructure resolves and validates the canonical root and constructs: @@ -318,14 +331,15 @@ validates the canonical root and constructs: AuthorizedOperationContext ├── invocation: PreAuthorizationContext ├── project_root -└── filesystem: RootedFilesystem +└── filesystem: FilesystemAccess ``` `project_root` is optional for process-scoped operations and represents the -canonical project or target root for scoped operations. Canonical containment -against `allowed_roots` is checked again before state-dependent validation or -side effects. `RootedFilesystem` is the policy-enforcing interface for -subsequent file access; a raw canonical path is identity, not authorization. +canonical project or target root for scoped operations. `RootBound` contexts +re-check canonical containment before state-dependent validation or side +effects and receive a `RootedFilesystem`. `HostUser` contexts receive a +`HostFilesystem` governed by operating-system permissions. A raw canonical +path is identity, not authorization under a root-bound policy. Shared operations must not call `os.chdir()` to establish request context. They pass resolved roots through operation phases and domain calls. Deadlines, @@ -333,9 +347,9 @@ cancellation, and output budgets are likewise explicit. ## Filesystem confinement -Root validation alone does not confine later access. A descendant can be a -symlink or junction to an outside path, and a path component can be replaced -between validation and use. +Under `RootBound`, root validation alone does not confine later access. A +descendant can be a symlink or junction to an outside path, and a path +component can be replaced between validation and use. Every filesystem access made under a confined policy must therefore use `RootedFilesystem` or an operation-owned equivalent that enforces the same @@ -357,6 +371,10 @@ unscoped filesystem helpers. Shared infrastructure may provide these root-bound primitives, but command-specific path semantics remain owned by the relevant hierarchy. +`HostUser` is an explicit adapter policy, not a confinement mechanism. It does +not imply force, overwrite consent, external-source trust, or permission for a +different adapter to use host-wide access. + ## Capability declarations Capabilities are independent requirements, not a highest-risk hierarchy: @@ -385,10 +403,10 @@ child process must either: The second capability is an explicit acknowledgement that the child runs with the server user's ambient privileges and can access paths outside -`allowed_roots`. It is never implied by `execution`, project cwd, machine mode, -or transport authentication. If sandboxing is required but unavailable, the -operation fails with a structured policy error; it must not silently fall back -to unrestricted execution. +a `RootBound` scope. It is never implied by `execution`, project cwd, machine +mode, or transport authentication. If sandboxing is required but unavailable, +the operation fails with a structured policy error; it must not silently fall +back to unrestricted execution. When `unrestricted-host-execution` is authorized, filesystem, environment, network, and subprocess restrictions cannot be claimed as enforced inside the @@ -426,19 +444,20 @@ Operation tests cover: - Warnings and structured errors. - Capabilities, trust, consent, network behavior, side effects, rollback, cancellation, and output bounds. -- Preliminary requested-root authorization and canonical post-resolution - containment, including symlink-escape rejection. +- Under `RootBound`, preliminary requested-root authorization and canonical + post-resolution containment, including symlink-escape rejection. - Root-bound enforcement at each descendant filesystem access, including symlink/junction replacement and time-of-check/time-of-use cases. - Sandboxed execution and explicit `unrestricted-host-execution` policy denial, with no unsafe fallback. - Domain behavior without Typer, Rich, MCP, or transport assertions. -Adapter tests cover invocation mapping and adapter-specific reporting. +Adapter tests cover invocation mapping, explicit filesystem scope and access +policy construction, and adapter-specific reporting. -Parity tests invoke CLI and MCP adapters against the same operation fixture -and compare semantic request, result, warning, error, and side-effect behavior. -Parity does not require byte-identical presentation. +Parity tests invoke CLI and MCP adapters against the same operation and policy +fixtures and compare semantic request, result, warning, error, and side-effect +behavior. Parity does not require byte-identical presentation. Behavioral changes follow [Testing deterministic behavior](../CONTRIBUTING.md#testing-deterministic-behavior): @@ -456,6 +475,8 @@ Avoid: - Adding adapter concepts to request, outcome, warning, or error models. - Hiding operations behind a central string dispatcher or service locator. - Letting adapters infer force, trust, consent, or extra capabilities. +- Leaving filesystem scope or default access policy implicit so shared code + must guess the adapter's authority. - Reading mutable process cwd instead of using invocation context. - Splitting simple operations or creating phase modules solely for symmetry. @@ -469,9 +490,11 @@ For an operation with CLI and MCP adapters: - [ ] Adapter modules contain only invocation, mapping, presentation, and adapter-specific concerns. - [ ] Shared request, outcome, warning, and error types are transport-neutral. +- [ ] Each adapter explicitly constructs its filesystem scope and access + policy. - [ ] Capability computation is pure and authorization precedes stateful work. -- [ ] Canonical project resolution and its allowed-root re-check occur only - after authorization. +- [ ] Under `RootBound`, canonical project resolution and its allowed-root + re-check occur only after authorization. - [ ] Every confined filesystem access remains anchored to authorized roots at the point of use. - [ ] Child processes are sandboxed or require explicit From 70ce370e063f8b406c2b09da85e7e1155499c7f0 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:12:06 -0500 Subject: [PATCH 10/19] docs: simplify adapter filesystem context Use one invocation context with host-user filesystem access for direct CLI and stdio MCP, while defining trusted read-only application resources and complete request policy state. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 13 +++++-- design/mcp.md | 93 +++++++++++++++++++++++++++++++---------------- design/shared.md | 94 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 137 insertions(+), 63 deletions(-) diff --git a/design/cli.md b/design/cli.md index ac4301d02e..7ddad90066 100644 --- a/design/cli.md +++ b/design/cli.md @@ -224,13 +224,16 @@ should remain reviewable in one place. ## Shared invocation context and direct CLI policy The direct local CLI constructs the -[shared pre-authorization context](shared.md#invocation-context) explicitly: +[shared invocation context](shared.md#invocation-context) explicitly: - `launch_working_directory` is captured once when the CLI invocation begins. - `requested_directory` comes from the command's typed input, or remains absent when project discovery should begin from the launch directory. - `filesystem_scope` is `HostUser`, preserving established CLI behavior for explicit paths such as an `init` target outside the launch directory. +- `filesystem` is the host-user filesystem interface. +- `application_resources` is the trusted read-only interface for the running + Specify distribution and validated source-checkout resources. - `access_policy` is `DirectCliPolicy`. - Deadline, cancellation, and output-budget values come from CLI invocation infrastructure rather than mutable command globals. @@ -239,7 +242,7 @@ The direct local CLI constructs the that user's operating-system permissions. It authorizes the capabilities declared for the selected operation, including ambient host execution when the descriptor requires `unrestricted-host-execution`. The shared operation does -not infer this policy and must not substitute MCP's launch-directory boundary. +not infer this policy or substitute another adapter's policy. This policy preserves host access, not semantic consent: @@ -282,7 +285,9 @@ command behavior. CLI adapter tests also verify `DirectCliPolicy` construction, including explicit targets outside the launch directory and the rule that machine modes -do not add force, trust, or destructive consent. +do not add force, trust, or destructive consent. They verify that distribution +metadata and bundled assets are supplied through the read-only application +resource interface rather than inferred from project filesystem scope. Shared operation and phase tests use `test_operation_.py` and `test_operation__.py` as defined in @@ -430,6 +435,8 @@ For a new or refactored command: adapter. - [ ] Direct CLI invocation constructs `HostUser` filesystem scope and `DirectCliPolicy`; embedded confinement is explicit. +- [ ] The CLI supplies the trusted read-only application-resource interface + independently from project filesystem scope. - [ ] CLI-private phase modules use `_command__.py`; shared phases use `_operation__.py`. - [ ] `_commands.py` contains only group infrastructure and genuinely shared diff --git a/design/mcp.md b/design/mcp.md index 6e3c13900a..d14a657102 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -247,16 +247,20 @@ The static disposition values are: - `excluded`: the command is intentionally not an MCP operation. Runtime policy state is separate from static inventory disposition. An -available tool has an `effective_state` of: +available tool has a request-specific `effective_state` of: -- `enabled`: the active policy authorizes the request's required - capabilities. +- `enabled`: the active policy authorizes the complete validated request, + including required capabilities, network access, filesystem scope and roots, + external-source trust policy, and unrestricted host execution. - `policy-disabled`: the tool remains discoverable, but invocation returns a - structured `policy_denied` error identifying the missing authorization. + structured `policy_denied` error identifying every denied policy dimension. -`effective_state` is derived from the active policy and request; it is not -stored as the inventory's static disposition. A metadata or describe surface -may report both fields but must preserve their distinct types. +`effective_state` is derived only after capability-free request validation and +the full request policy decision; it is not stored as the inventory's static +disposition. A metadata or describe surface without a concrete request reports +the static disposition and active policy constraints, not a fabricated +`effective_state`. A request-evaluation surface may report both fields but must +preserve their distinct types. Every CLI leaf must appear exactly once. The inventory parity test fails for a missing leaf, duplicate logical operation, duplicate tool name, stale CLI @@ -315,10 +319,18 @@ logged only through the MCP diagnostic channel. ## Invocation context and project resolution The MCP adapter constructs the immutable -[shared pre-authorization context](shared.md#invocation-context) from the -requested directory, server launch state, host roots, active policy, and call -lifecycle. Its filesystem scope is `RootBound(allowed_roots)`. It performs no -project discovery or filesystem resolution while constructing that context. +[shared invocation context](shared.md#invocation-context) from the requested +directory, server launch state, active policy, and call lifecycle. Local stdio +uses `HostUser` filesystem access by default, like the direct CLI. A host may +explicitly configure `RootBound(allowed_roots)`; the adapter never derives that +boundary from cwd. Context construction performs no project discovery or +filesystem resolution. + +MCP server composition also binds `application_resources` to distribution +metadata and validated first-party assets through standard package-resource +APIs, with a validated source/editable-layout fallback. Tool schemas never +accept an application-resource root or raw package path. The same interface is +available under stdio and future Streamable HTTP transports. Project-scoped MCP tools accept an optional project directory when their use case needs one. If omitted, project discovery starts from the server launch @@ -331,24 +343,23 @@ The invocation follows these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Pass the unresolved requested directory and - `RootBound(host_provided_allowed_roots)` explicitly. +- Pass the unresolved requested directory and explicitly selected filesystem + scope. +- Pass trusted application resources separately from the project filesystem; + use them only after `local-read` authorization. - Perform preliminary policy checks without filesystem access, then resolve the canonical project root under authorized `local-read`. -- Re-check canonical allowed-root containment after resolution as an admission - check. -- Pass the authorized root and root-bound filesystem interface through +- Pass the resolved project root and selected filesystem interface through operation phases. -- Enforce containment at every descendant access with descriptor-/handle-based - no-follow traversal or an equivalent fail-closed platform mechanism; do not - rely on the one root check to prevent later symlink, junction, or path-swap - escapes. +- Under `RootBound`, re-check canonical containment after resolution and + enforce it at every descendant access with descriptor-/handle-based no-follow + traversal or an equivalent fail-closed platform mechanism. - Do not infer the project from an unrelated server process state after the invocation begins. `init` is a special project-creation operation: its request identifies the target directory, while the context identifies the launch directory and -allowed roots. +filesystem scope. ## Non-interactive behavior @@ -369,7 +380,7 @@ or `execution` does not imply: - `force=true`. - Trust of an external URL or downloaded executable content. - Permission to overwrite user-modified files. -- Permission to leave the declared project root. +- Under `RootBound`, permission to leave the declared project root. - Permission to execute a workflow, hook, installer, or arbitrary command. - Permission for a child process to use unrestricted host access. @@ -415,9 +426,10 @@ default policy is conservative: authorization. - `unrestricted-host-execution` requires a separate explicit authorization and remains default-deny. -- In-process filesystem access is limited to host-provided roots or, when none - are provided, the server launch working directory, with the boundary enforced - at each access. +- Local stdio uses `HostUser` filesystem access, matching direct CLI behavior. +- A host-configured `RootBound` scope is enforced at every access. A future + remote transport must select its filesystem scope explicitly and must not + inherit stdio's host-user default accidentally. - Server-managed and sandboxed network access is denied unless explicitly enabled. An authorized unrestricted host child is a disclosed broad exception, not a network-confined execution mode. @@ -427,7 +439,7 @@ Tool annotations should conservatively reflect the full declared capability set, but annotations do not replace server-side enforcement. If policy denies any capability required by an otherwise implemented tool request, the registered tool returns a structured `policy_denied` error identifying the -missing capabilities. A runtime describe surface may report +denied policy dimensions. A request-evaluation surface may report `effective_state: policy-disabled`; the static inventory disposition remains `available`. @@ -510,6 +522,7 @@ src/specify_cli/mcp_server/ ├── registry.py ├── policy.py ├── context.py +├── resources.py └── transports/ ├── stdio.py └── streamable_http.py @@ -518,6 +531,10 @@ src/specify_cli/mcp_server/ Create only the modules justified by implemented behavior. The layout defines an architectural boundary, not a requirement to add empty files. +`resources.py` binds the shared read-only application-resource interface to the +running Specify distribution. It does not own command-specific asset selection +or expose package paths as project roots. + ## Testing structure Shared operation, CLI adapter, and parity coverage follows @@ -532,6 +549,9 @@ Shared operation, CLI adapter, and parity coverage follows - Verify access-policy, trust, timeout, cancellation, and output-budget failures. - Verify descendant filesystem escapes are rejected at the point of access. +- Verify installed metadata and bundled assets remain readable through the + read-only application-resource interface when package paths are outside + project roots, and cannot be written or selected by caller path. - Verify unsandboxed child execution requires `unrestricted-host-execution` and never occurs as a fallback. @@ -542,6 +562,9 @@ Shared operation, CLI adapter, and parity coverage follows - Reject duplicate operation IDs and MCP tool names. - Require reasons for every unavailable or excluded command. - Verify available tools are registered by the owning hierarchy. +- Verify request-specific `effective_state` uses the complete policy decision, + including a network-required request whose capabilities are authorized but + whose network access is denied. - Preserve total pytest collection when tests move, as required by the CLI architecture. @@ -586,10 +609,11 @@ network_access: none project_scope: process ``` -`_operation_version.py` owns typed version collection and `VersionResult`. -`command_version.py` renders the panel, feature text, or established JSON -object. `mcp_version.py` returns the same result fields as structured content. -No adapter starts a child process. +`_operation_version.py` reads distribution metadata through +`ReadOnlyApplicationResources` and owns typed version collection and +`VersionResult`. `command_version.py` renders the panel, feature text, or +established JSON object. `mcp_version.py` returns the same result fields as +structured content. No adapter starts a child process. ### `artifact list`: project-scoped read @@ -671,6 +695,9 @@ never prompts and never turns its machine context into force or trust. The shared operation validates inputs, builds a plan, applies transactional changes, and returns created/updated paths plus structured warnings. The CLI adapter may gather interactive choices before constructing the same request. +Bundled templates and scripts are read through +`ReadOnlyApplicationResources`; created project files use the authorized +project filesystem interface. `init` tool checks launch host binaries, so this example conservatively requires `unrestricted-host-execution`. A sandboxed implementation may omit @@ -705,6 +732,8 @@ Avoid: hierarchy. - Adding operation phases or `_mcp.py` files solely for symmetry. - Silently omitting CLI leaves from the MCP inventory. +- Adding installed package or source-checkout resource paths to + request-writable roots. - Returning partial, truncated, or fallback data as a successful complete result. - Letting transport concerns leak into command contracts. @@ -721,8 +750,12 @@ For a new or migrated MCP operation: - [ ] Availability, possible and request-required capabilities, network access, project scope, and contract version are explicit. +- [ ] Request-specific `effective_state` reflects the complete policy decision, + not capabilities alone. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. +- [ ] Distribution metadata and bundled first-party assets use the trusted + read-only application-resource interface, not project roots. - [ ] Timeouts, cancellation, stdin, and output bounds are handled. - [ ] Existing CLI human and JSON behavior remains compatible. - [ ] Operation, CLI adapter, MCP adapter, parity, and protocol tests cover diff --git a/design/shared.md b/design/shared.md index 8b98c4a0da..d80e874c01 100644 --- a/design/shared.md +++ b/design/shared.md @@ -21,9 +21,9 @@ The shared layer optimizes for: explicit and testable. - **Transport neutrality:** shared contracts contain no Typer, Rich, MCP, stdout/stderr, protocol, or exit-code concerns. -- **Explicit context:** requested directories, authorized project roots, - policy, deadlines, cancellation, and output budgets are passed rather than - read from mutable process globals. +- **Explicit context:** requested directories, resolved project roots, policy, + filesystem authority, deadlines, cancellation, and output budgets are passed + rather than read from mutable process globals. - **Reviewable ownership:** application behavior has a predictable source and mirrored tests. @@ -77,6 +77,8 @@ An adapter must not: | Capability-free and state-dependent validation | Shared operation | | Application orchestration and side effects | Shared operation and domain modules | | Operation-private phases | `_operation__.py` | +| Read-only application-resource interface | Shared application infrastructure | +| Binding that interface to the running distribution | Delivery-adapter composition | | Invocation syntax and presentation | Delivery adapter | | Human prompting and terminal rendering | CLI adapter | | MCP tool schemas, annotations, and tool errors | MCP adapter | @@ -294,13 +296,15 @@ Contract evolution follows these rules: ## Invocation context -Adapters begin with an immutable, I/O-free pre-authorization context: +Adapters construct one immutable invocation context: ```text -PreAuthorizationContext +InvocationContext ├── launch_working_directory ├── requested_directory ├── filesystem_scope +├── filesystem: FilesystemAccess +├── application_resources: ReadOnlyApplicationResources ├── access_policy ├── deadline ├── cancellation @@ -308,9 +312,8 @@ PreAuthorizationContext ``` `requested_directory` is the caller-supplied value, or absent when discovery -should begin from `launch_working_directory`. Constructing this context must -not read the environment, inspect the filesystem, resolve symlinks, or discover -a project. +should begin from `launch_working_directory`. Constructing the context does not +resolve or validate a project. `filesystem_scope` is explicit: @@ -319,27 +322,22 @@ RootBound(allowed_roots) HostUser ``` -`RootBound` confines access to host-provided roots. `HostUser` preserves the -direct local CLI model: filesystem access is governed by the invoking user's -operating-system permissions rather than an application root boundary. An -adapter must choose one; absence of a scope never means unrestricted access. +`RootBound` confines access to host-provided roots and supplies a +`RootedFilesystem`. `HostUser` supplies a `HostFilesystem` governed by the +invoking user's operating-system permissions. Direct CLI and local stdio MCP +use `HostUser` by default. A host may deliberately configure `RootBound`; an +adapter must never infer it from cwd or silently switch scopes. -After policy authorization, shared application infrastructure resolves and -validates the canonical root and constructs: +`application_resources` is bound during trusted adapter/process composition to +the running Specify distribution, never from request data. -```text -AuthorizedOperationContext -├── invocation: PreAuthorizationContext -├── project_root -└── filesystem: FilesystemAccess -``` - -`project_root` is optional for process-scoped operations and represents the -canonical project or target root for scoped operations. `RootBound` contexts -re-check canonical containment before state-dependent validation or side -effects and receive a `RootedFilesystem`. `HostUser` contexts receive a -`HostFilesystem` governed by operating-system permissions. A raw canonical -path is identity, not authorization under a root-bound policy. +The parsing and capability-computation phases do not call `filesystem` or +`application_resources`. After policy authorization, the shared operation uses +those interfaces to resolve and validate the canonical project or target root, +then passes the resolved root explicitly through operation phases. +`RootBound` re-checks canonical containment before state-dependent validation +or side effects. A raw canonical path is identity, not authorization under a +root-bound policy. Shared operations must not call `os.chdir()` to establish request context. They pass resolved roots through operation phases and domain calls. Deadlines, @@ -375,16 +373,46 @@ relevant hierarchy. not imply force, overwrite consent, external-source trust, or permission for a different adapter to use host-wide access. +## Trusted application resources + +Installed package metadata and first-party bundled assets are a separate +read-only authority domain from caller-selected project paths. They may live +outside an MCP server's `RootBound` roots in wheel, pipx, source-checkout, and +editable installations. + +After `local-read` authorization, operations may use +`ReadOnlyApplicationResources`. The interface: + +- Reads distribution metadata and validated first-party bundled resources by + logical identifier, not arbitrary caller-supplied path. +- Restricts backing locations to the running Specify distribution and its + validated source-checkout resource roots. +- Exposes no create, update, delete, rename, or arbitrary path traversal API. +- Rejects malformed or unknown identifiers and fails closed when a resource + cannot be validated. +- Keeps downloaded catalogs, third-party extensions, user configuration, and + project files outside this trusted resource domain. + +Operations must not add package installation paths to request-writable allowed +roots merely to read application assets. For example, `version` reads +distribution metadata through `application_resources`; `init` reads bundled +templates and scripts through that interface and writes them through +`filesystem`. + +Both adapters provide the application-resource interface explicitly. Shared +operations do not discover package roots from cwd, environment variables, or +caller input. + ## Capability declarations Capabilities are independent requirements, not a highest-risk hierarchy: | Capability | Meaning | | --- | --- | -| `local-read` | Read process, host, or project state within allowed roots | -| `project-write` | Create or change project/target files or configuration | +| `local-read` | Read process, application, host, or project state permitted by the selected filesystem scope | +| `project-write` | Create or change files or configuration permitted by the selected filesystem scope | | `execution` | Start host tools, workflows, hooks, agents, or processes | -| `unrestricted-host-execution` | Permit an executed child to use the server user's ambient host access outside allowed roots | +| `unrestricted-host-execution` | Permit an executed child to use the server user's ambient host access outside configured confinement | | `self-modifying` | Change the Specify installation or machine-level state | The descriptor declares the conservative union an operation may require. @@ -448,6 +476,8 @@ Operation tests cover: post-resolution containment, including symlink-escape rejection. - Root-bound enforcement at each descendant filesystem access, including symlink/junction replacement and time-of-check/time-of-use cases. +- Read-only application resources in wheel/pipx and source/editable layouts, + including rejection of caller-controlled paths and write attempts. - Sandboxed execution and explicit `unrestricted-host-execution` policy denial, with no unsafe fallback. - Domain behavior without Typer, Rich, MCP, or transport assertions. @@ -477,6 +507,8 @@ Avoid: - Letting adapters infer force, trust, consent, or extra capabilities. - Leaving filesystem scope or default access policy implicit so shared code must guess the adapter's authority. +- Adding installed package roots to project-writable roots instead of using + the read-only application-resource interface. - Reading mutable process cwd instead of using invocation context. - Splitting simple operations or creating phase modules solely for symmetry. @@ -492,6 +524,8 @@ For an operation with CLI and MCP adapters: - [ ] Shared request, outcome, warning, and error types are transport-neutral. - [ ] Each adapter explicitly constructs its filesystem scope and access policy. +- [ ] Trusted package metadata and bundled assets use + `ReadOnlyApplicationResources`, not project filesystem authority. - [ ] Capability computation is pure and authorization precedes stateful work. - [ ] Under `RootBound`, canonical project resolution and its allowed-root re-check occur only after authorization. From 834bb59b792f83fe66ddd36b7f3e7055b8946c31 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:00:27 -0500 Subject: [PATCH 11/19] docs: simplify shared command layering Limit the architecture to shared application operations plus CLI and MCP adapters, removing universal context, filesystem, and resource abstractions. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 61 +++---- design/mcp.md | 184 ++++++++------------- design/shared.md | 409 +++++++++++++++++------------------------------ 3 files changed, 234 insertions(+), 420 deletions(-) diff --git a/design/cli.md b/design/cli.md index 7ddad90066..c7f92907c4 100644 --- a/design/cli.md +++ b/design/cli.md @@ -221,41 +221,26 @@ Registration imports should be explicit and ordered consistently. Do not rely on filesystem discovery to import arbitrary modules, because command exposure should remain reviewable in one place. -## Shared invocation context and direct CLI policy - -The direct local CLI constructs the -[shared invocation context](shared.md#invocation-context) explicitly: - -- `launch_working_directory` is captured once when the CLI invocation begins. -- `requested_directory` comes from the command's typed input, or remains absent - when project discovery should begin from the launch directory. -- `filesystem_scope` is `HostUser`, preserving established CLI behavior for - explicit paths such as an `init` target outside the launch directory. -- `filesystem` is the host-user filesystem interface. -- `application_resources` is the trusted read-only interface for the running - Specify distribution and validated source-checkout resources. -- `access_policy` is `DirectCliPolicy`. -- Deadline, cancellation, and output-budget values come from CLI invocation - infrastructure rather than mutable command globals. - -`DirectCliPolicy` represents a user intentionally running a local command under -that user's operating-system permissions. It authorizes the capabilities -declared for the selected operation, including ambient host execution when the -descriptor requires `unrestricted-host-execution`. The shared operation does -not infer this policy or substitute another adapter's policy. - -This policy preserves host access, not semantic consent: +## Shared operation invocation + +The CLI adapter maps parsed arguments and options into the shared typed request, +invokes the operation, and renders its outcome. + +For project-scoped commands, the adapter supplies the explicit project or +target directory. When the user omits it, the CLI uses the cwd captured when +the invocation begins. The shared operation validates and resolves that path; +neither layer changes process-wide cwd. + +The CLI runs with the invoking user's ordinary operating-system permissions. +It does not construct a shared policy or filesystem runtime. Prompting remains +CLI-specific: - `--json`, `--non-interactive`, and local invocation do not imply `force`. - External-source trust and overwrite consent remain explicit request values. -- Command-specific prompts may collect consent before constructing a new - request, but the adapter does not add consent silently. -- Network defaults and self-modifying behavior remain those explicitly defined - by the selected operation and its CLI contract. - -An embedded CLI host may supply a `RootBound` scope and a stricter access policy -instead. Adapter parity compares equivalent requests under equivalent policy -contexts; a policy denial is not an application-behavior divergence. +- A prompt may collect consent before constructing or retrying the request, but + the adapter does not add consent silently. +- Network and self-modifying behavior remain those defined by the operation and + CLI contract. ## Test structure @@ -283,11 +268,8 @@ The primary `test_command_.py` suite verifies the public command surface. Phase-specific suites verify detailed invariants without obscuring the primary command behavior. -CLI adapter tests also verify `DirectCliPolicy` construction, including -explicit targets outside the launch directory and the rule that machine modes -do not add force, trust, or destructive consent. They verify that distribution -metadata and bundled assets are supplied through the read-only application -resource interface rather than inferred from project filesystem scope. +CLI adapter tests verify explicit targets outside the launch directory and the +rule that machine modes do not add force, trust, or destructive consent. Shared operation and phase tests use `test_operation_.py` and `test_operation__.py` as defined in @@ -433,10 +415,7 @@ For a new or refactored command: - [ ] The adapter maps into the shared operation defined by `design/shared.md`. - [ ] Semantic validation, orchestration, and side effects are below the CLI adapter. -- [ ] Direct CLI invocation constructs `HostUser` filesystem scope and - `DirectCliPolicy`; embedded confinement is explicit. -- [ ] The CLI supplies the trusted read-only application-resource interface - independently from project filesystem scope. +- [ ] Project and target directories map explicitly into the shared request. - [ ] CLI-private phase modules use `_command__.py`; shared phases use `_operation__.py`. - [ ] `_commands.py` contains only group infrastructure and genuinely shared diff --git a/design/mcp.md b/design/mcp.md index d14a657102..2abc258bf6 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -250,8 +250,8 @@ Runtime policy state is separate from static inventory disposition. An available tool has a request-specific `effective_state` of: - `enabled`: the active policy authorizes the complete validated request, - including required capabilities, network access, filesystem scope and roots, - external-source trust policy, and unrestricted host execution. + including required capabilities, network access, and external-source trust + policy. - `policy-disabled`: the tool remains discoverable, but invocation returns a structured `policy_denied` error identifying every denied policy dimension. @@ -302,8 +302,8 @@ The MCP adapter projects that contract onto MCP: - Its input schema is command-specific and maps into the shared typed request. - It performs protocol/schema validation but no state-dependent semantic work. -- It invokes the shared validation and authorization lifecycle before - application behavior. +- It applies MCP policy to the shared operation's declared requirements before + invoking application behavior. - It maps the shared result and warnings into command-specific structured content. - It maps expected shared errors into MCP tool errors without adding @@ -316,50 +316,34 @@ The command/domain hierarchy continues to own the semantic result shape. Unexpected exceptions become sanitized `internal_error` tool failures and are logged only through the MCP diagnostic channel. -## Invocation context and project resolution - -The MCP adapter constructs the immutable -[shared invocation context](shared.md#invocation-context) from the requested -directory, server launch state, active policy, and call lifecycle. Local stdio -uses `HostUser` filesystem access by default, like the direct CLI. A host may -explicitly configure `RootBound(allowed_roots)`; the adapter never derives that -boundary from cwd. Context construction performs no project discovery or -filesystem resolution. - -MCP server composition also binds `application_resources` to distribution -metadata and validated first-party assets through standard package-resource -APIs, with a validated source/editable-layout fallback. Tool schemas never -accept an application-resource root or raw package path. The same interface is -available under stdio and future Streamable HTTP transports. +## Project directory and execution environment Project-scoped MCP tools accept an optional project directory when their use case needs one. If omitted, project discovery starts from the server launch -working directory, matching normal CLI behavior. The adapter carries that -requested context into the shared request; after capability authorization, the -shared operation resolves the project through the same domain helper used by -CLI and produces the same semantic errors. +working directory, matching normal CLI behavior. The adapter maps that explicit +path into the shared request. The shared operation resolves it through the same +domain helper used by CLI and produces the same semantic errors. -The invocation follows these rules: +The adapter and shared operation follow these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Pass the unresolved requested directory and explicitly selected filesystem - scope. -- Pass trusted application resources separately from the project filesystem; - use them only after `local-read` authorization. -- Perform preliminary policy checks without filesystem access, then resolve - the canonical project root under authorized `local-read`. -- Pass the resolved project root and selected filesystem interface through - operation phases. -- Under `RootBound`, re-check canonical containment after resolution and - enforce it at every descendant access with descriptor-/handle-based no-follow - traversal or an equivalent fail-closed platform mechanism. +- Authorize the operation's declared capabilities and network requirements + before invoking behavior that uses them. +- Pass the resolved project or target path explicitly through operation phases. +- Use the same shared Python/domain helpers as CLI for distribution metadata, + bundled assets, project files, and other application behavior. - Do not infer the project from an unrelated server process state after the invocation begins. `init` is a special project-creation operation: its request identifies the -target directory, while the context identifies the launch directory and -filesystem scope. +target directory rather than an existing project root. + +Local stdio runs with the operating-system permissions of the server process, +just as the CLI runs with its process user's permissions. The command +architecture does not claim to provide a per-operation filesystem sandbox. A +host that needs confinement runs the MCP server inside an OS sandbox, container, +restricted account, or equivalent transport-host boundary. ## Non-interactive behavior @@ -373,20 +357,18 @@ MCP operations are always non-interactive: - If no safe default exists, return a structured `input_required` or `confirmation_required` error explaining which field must be supplied. -Machine mode is not consent. An MCP call, `--json`, `--non-interactive`, a -host confirmation dialog, or authorization of `local-read`, `project-write`, -or `execution` does not imply: +Machine mode is not consent. Starting MCP, using `--json` or +`--non-interactive`, or receiving a host confirmation does not imply: - `force=true`. - Trust of an external URL or downloaded executable content. - Permission to overwrite user-modified files. -- Under `RootBound`, permission to leave the declared project root. -- Permission to execute a workflow, hook, installer, or arbitrary command. -- Permission for a child process to use unrestricted host access. +- Permission to execute behavior that the caller did not explicitly request. -Consent must be explicit in the command request and valid under the active -access policy. MCP annotations and host UI are advisory; the server still -enforces operation requirements. +The caller must explicitly invoke an execution operation, supply any +command-specific consent fields, and satisfy the active access policy. MCP +annotations and host UI are advisory; the server still enforces operation +requirements. ## Capability requirements and access policy @@ -397,18 +379,11 @@ is not sufficient. Examples: ```text version -> {local-read} -check -> {local-read, execution, unrestricted-host-execution} -workflow.run -> {local-read, project-write, execution, - unrestricted-host-execution} -self.upgrade -> {local-read, execution, unrestricted-host-execution, - self-modifying} +check -> {local-read, execution} +workflow.run -> {local-read, project-write, execution} +self.upgrade -> {local-read, execution, self-modifying} ``` -These examples conservatively classify their child processes as using ambient -host privileges. An implementation may omit `unrestricted-host-execution` only -when an enforceable sandbox contains the child within its authorized -filesystem, network, environment, and subprocess boundaries. - `read-only` is a derived description, not an authorizable capability. A request is read-only only when it requires no `project-write`, `execution`, or `self-modifying` capability. An operation that launches a binary is therefore @@ -424,15 +399,7 @@ default policy is conservative: - `local-read` is authorized. - `project-write`, `execution`, and `self-modifying` require explicit authorization. -- `unrestricted-host-execution` requires a separate explicit authorization and - remains default-deny. -- Local stdio uses `HostUser` filesystem access, matching direct CLI behavior. -- A host-configured `RootBound` scope is enforced at every access. A future - remote transport must select its filesystem scope explicitly and must not - inherit stdio's host-user default accidentally. -- Server-managed and sandboxed network access is denied unless explicitly - enabled. An authorized unrestricted host child is a disclosed broad - exception, not a network-confined execution mode. +- Network access is denied unless explicitly enabled. - External-source trust is separately controlled and remains default-deny. Tool annotations should conservatively reflect the full declared capability @@ -448,12 +415,10 @@ optional, expected to be idempotent, or combined with a more powerful capability. MCP enforces the shared operation's static and request-specific declarations; it does not infer or reduce them independently. -An MCP execution request must use a host sandbox that enforces its authorized -filesystem, network, environment, and subprocess boundaries. If the -implementation launches a child with ambient server-user access instead, the -operation descriptor and request-specific capability calculation must require -`unrestricted-host-execution`. The server must deny the request when that -separate grant is absent; setting cwd inside the project is not confinement. +`execution` authorizes an operation to launch a child process. That child runs +with the MCP server process user's privileges unless the host externally +sandboxes the server. Setting cwd inside a project is not a security boundary, +and this command architecture does not claim otherwise. ## Trust, confirmation, and network responsibilities @@ -470,19 +435,20 @@ confirmation. The adapters own how explicit consent enters the request. behavior, shared by both adapters. - Transport authentication does not replace operation authorization. -Network calls use bounded connect/read timeouts and honor the invocation -deadline. Operations do not silently switch from offline to online behavior. -When a request supports offline behavior, the input states it explicitly or -uses the same documented default as the CLI. +Network calls use bounded connect/read timeouts. Operations do not silently +switch from offline to online behavior. When a request supports offline +behavior, the input states it explicitly or uses the same documented default +as the CLI. ## Timeouts, cancellation, stdin, and bounded output -The transport adapter creates the invocation deadline and cancellation signal; -the operation and its phases honor them. +The transport adapter owns protocol deadlines, cancellation, and response-size +enforcement. Operations expose focused support only when their behavior can +cooperate with it. -- Subprocesses and network calls receive a timeout derived from the remaining - deadline. -- Complex operations check cancellation between cohesive phases. +- Subprocesses and network calls receive explicit timeouts. +- A complex operation may accept a cancellation token or deadline as a focused + parameter; there is no universal invocation context. - Transactional mutations roll back or report partial state according to their domain contract. - Cancellation returns a structured cancellation error, never a successful @@ -492,8 +458,8 @@ the operation and its phases honor them. - Potentially large lists use command-owned limits or pagination. - Truncation is explicit and includes a continuation cursor or a clear `truncated` marker; it is never silent. -- Output-budget enforcement belongs to shared invocation infrastructure, while - pagination semantics belong to the command hierarchy. +- MCP response-size enforcement belongs to the adapter; pagination semantics + belong to the command hierarchy. ## Transport separation @@ -521,8 +487,6 @@ src/specify_cli/mcp_server/ ├── server.py ├── registry.py ├── policy.py -├── context.py -├── resources.py └── transports/ ├── stdio.py └── streamable_http.py @@ -531,10 +495,6 @@ src/specify_cli/mcp_server/ Create only the modules justified by implemented behavior. The layout defines an architectural boundary, not a requirement to add empty files. -`resources.py` binds the shared read-only application-resource interface to the -running Specify distribution. It does not own command-specific asset selection -or expose package paths as project roots. - ## Testing structure Shared operation, CLI adapter, and parity coverage follows @@ -548,12 +508,9 @@ Shared operation, CLI adapter, and parity coverage follows - Verify structured warnings and tool errors. - Verify access-policy, trust, timeout, cancellation, and output-budget failures. -- Verify descendant filesystem escapes are rejected at the point of access. -- Verify installed metadata and bundled assets remain readable through the - read-only application-resource interface when package paths are outside - project roots, and cannot be written or selected by caller path. -- Verify unsandboxed child execution requires - `unrestricted-host-execution` and never occurs as a fallback. +- Verify project-directory mapping and operation dispatch without `os.chdir()`. +- Verify execution and network operations are denied when their declared + requirements are not authorized. ### Inventory tests @@ -609,11 +566,10 @@ network_access: none project_scope: process ``` -`_operation_version.py` reads distribution metadata through -`ReadOnlyApplicationResources` and owns typed version collection and -`VersionResult`. `command_version.py` renders the panel, feature text, or -established JSON object. `mcp_version.py` returns the same result fields as -structured content. No adapter starts a child process. +`_operation_version.py` owns typed version collection and `VersionResult`, +using the normal shared version/domain helper. `command_version.py` renders the +panel, feature text, or established JSON object. `mcp_version.py` returns the +same result fields as structured content. No adapter starts a child process. ### `artifact list`: project-scoped read @@ -643,14 +599,12 @@ network_access: none project_scope: required ``` -The request contains an optional project directory. After authorization, -project resolution produces a canonical root in the authorized operation -context, re-checks allowed-root containment, and uses the root-bound filesystem -interface for every descendant access. `_operation_list.py` uses -`ArtifactCatalog` and returns typed artifact rows. The CLI adapter preserves -its JSON stream contract; the MCP adapter exposes the rows through its output -schema and never captures CLI stdout. Invocation output budgets must produce -explicit bounded-output behavior rather than silent truncation. +The request contains an optional project directory. After authorization, the +shared operation resolves and validates that path, uses `ArtifactCatalog`, and +returns typed artifact rows. The CLI adapter preserves its JSON stream +contract; the MCP adapter exposes the rows through its output schema and never +captures CLI stdout. Large results use explicit operation limits or pagination +rather than silent truncation. ### `init`: complex project mutation @@ -683,8 +637,7 @@ Contract: operation_id: init cli_path: specify init mcp_tool_name: specify_init -capabilities: [local-read, project-write, execution, - unrestricted-host-execution] +capabilities: [local-read, project-write, execution] network_access: optional project_scope: creates-target ``` @@ -695,14 +648,7 @@ never prompts and never turns its machine context into force or trust. The shared operation validates inputs, builds a plan, applies transactional changes, and returns created/updated paths plus structured warnings. The CLI adapter may gather interactive choices before constructing the same request. -Bundled templates and scripts are read through -`ReadOnlyApplicationResources`; created project files use the authorized -project filesystem interface. - -`init` tool checks launch host binaries, so this example conservatively -requires `unrestricted-host-execution`. A sandboxed implementation may omit -that capability, but it must not infer the grant from `execution` or silently -fall back when a sandbox is unavailable. +Bundled templates and scripts use the same shared asset helpers as CLI. If the target is non-empty and `force` is false, both adapters receive the same semantic confirmation-required failure. The CLI may respond by prompting and @@ -732,8 +678,6 @@ Avoid: hierarchy. - Adding operation phases or `_mcp.py` files solely for symmetry. - Silently omitting CLI leaves from the MCP inventory. -- Adding installed package or source-checkout resource paths to - request-writable roots. - Returning partial, truncated, or fallback data as a successful complete result. - Letting transport concerns leak into command contracts. @@ -754,8 +698,6 @@ For a new or migrated MCP operation: not capabilities alone. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. -- [ ] Distribution metadata and bundled first-party assets use the trusted - read-only application-resource interface, not project roots. - [ ] Timeouts, cancellation, stdin, and output bounds are handled. - [ ] Existing CLI human and JSON behavior remains compatible. - [ ] Operation, CLI adapter, MCP adapter, parity, and protocol tests cover diff --git a/design/shared.md b/design/shared.md index d80e874c01..25433c6acb 100644 --- a/design/shared.md +++ b/design/shared.md @@ -2,8 +2,8 @@ This document defines the application layer beneath Specify delivery adapters. A logical operation has one shared implementation. The CLI, MCP, and any future -delivery surface translate their own inputs into that operation and translate -its outcome into their own output and error conventions. +delivery surface translate their inputs into that operation and translate its +outcome into their own output and error conventions. [Specify CLI Command Architecture](cli.md) defines the Typer/terminal adapter. [Specify MCP Command Architecture](mcp.md) defines MCP tool exposure, policy, @@ -19,11 +19,10 @@ The shared layer optimizes for: calling or parsing one another. - **Typed contracts:** requests, results, warnings, and expected errors are explicit and testable. -- **Transport neutrality:** shared contracts contain no Typer, Rich, MCP, - stdout/stderr, protocol, or exit-code concerns. -- **Explicit context:** requested directories, resolved project roots, policy, - filesystem authority, deadlines, cancellation, and output budgets are passed - rather than read from mutable process globals. +- **Lean boundaries:** shared code contains application behavior, not a + universal runtime, service locator, transport abstraction, or policy engine. +- **Explicit paths:** project and target paths are passed as operation inputs + rather than established through mutable process cwd. - **Reviewable ownership:** application behavior has a predictable source and mirrored tests. @@ -34,12 +33,12 @@ The shared layer does not: - Standardize how adapters spell arguments, display progress, or format human output. - Require byte-identical CLI JSON and MCP protocol envelopes. -- Turn the shared operation into a universal string-based command dispatcher. -- Replace focused domain modules with a central service locator. -- Make transport authentication, MCP authorization, or CLI prompting part of - domain behavior. -- Require extra modules for a small operation when an existing Typer-free - domain module is already the correct owner. +- Turn operations into a universal string-based command dispatcher. +- Replace focused domain modules with a central execution engine. +- Define CLI prompting, MCP authorization, transport authentication, or host + sandboxing. +- Require a context object or extra module when ordinary typed parameters and + an existing domain module are sufficient. ## One logical operation, multiple adapters @@ -47,47 +46,42 @@ Adapters are peers above one application operation: ```text CLI arguments/options ─┐ -MCP tool input JSON ───┼─> typed operation request -> shared behavior -future adapter input ──┘ -> typed operation outcome +MCP tool input JSON ───┼─> typed request -> shared operation -> typed outcome +future adapter input ──┘ -typed operation outcome ─┬─> CLI text, JSON, warnings, and exit status - ├─> MCP structured content or tool error - └─> future adapter representation +typed outcome ─────────┬─> CLI text, JSON, warnings, and exit status + ├─> MCP structured content or tool error + └─> future adapter representation ``` -The shared operation is the semantic source of truth. Adapters may expose -different presentation features, but equivalent requests under equivalent -authorized contexts must produce equivalent results, warnings, expected -failures, and side effects. +The shared operation is the semantic source of truth. Equivalent requests +produce equivalent results, warnings, expected failures, and side effects. An adapter must not: - Invoke another adapter. - Parse another adapter's output. -- Reimplement application orchestration. -- Add semantic defaults, trust, consent, or side effects that are absent from - the shared request. +- Reimplement semantic validation or application orchestration. +- Add semantic defaults, trust, consent, or side effects absent from the + shared request. ## Ownership boundaries | Concern | Owner | | --- | --- | -| Logical operation ID and contract version | Shared operation descriptor | -| Semantic request/result/warning/error models | Relevant command/domain hierarchy | -| Capability-free and state-dependent validation | Shared operation | -| Application orchestration and side effects | Shared operation and domain modules | +| Logical operation ID and contract version | Relevant command/domain hierarchy | +| Typed request, result, warning, and error models | Relevant command/domain hierarchy | +| Semantic validation, orchestration, and side effects | Shared operation and domain modules | | Operation-private phases | `_operation__.py` | -| Read-only application-resource interface | Shared application infrastructure | -| Binding that interface to the running distribution | Delivery-adapter composition | -| Invocation syntax and presentation | Delivery adapter | -| Human prompting and terminal rendering | CLI adapter | -| MCP tool schemas, annotations, and tool errors | MCP adapter | -| CLI exit-code mapping | CLI adapter | -| MCP access-policy enforcement and transport | MCP infrastructure | - -Domain behavior that already has a focused Typer-free owner may remain there. -A dedicated `_operation_.py` coordinates domain calls when the adapter -otherwise would own semantic validation or orchestration. +| Project and target path validation | Shared operation or focused domain helper | +| Bundled assets and distribution metadata | Existing shared/domain resource helpers | +| CLI syntax, prompting, rendering, JSON, and exit codes | CLI adapter | +| MCP schemas, policy enforcement, annotations, and tool errors | MCP adapter | +| MCP server lifecycle and transport | MCP infrastructure | + +Domain behavior that already has a focused Typer-free owner remains there. A +dedicated `_operation_.py` coordinates domain calls only when an adapter +would otherwise own semantic validation or orchestration. ## Logical operation identity @@ -108,7 +102,7 @@ CLI: specify artifact list MCP: specify_artifact_list ``` -An operation descriptor declares at least: +An operation descriptor declares the metadata shared by adapters: ```text operation_id @@ -123,12 +117,13 @@ project_scope default_timeout ``` -Adapter-specific registration metadata extends this descriptor without moving -the shared fields into adapter infrastructure. +Adapter registration extends this metadata without moving command-specific +contracts into central infrastructure. ## Naming and layout -When a focused shared application entry point is required, use: +Use an existing focused domain module when it already provides the correct +Typer-free application entry point. Otherwise use: ```text _operation_.py @@ -143,8 +138,8 @@ src/specify_cli/ └── mcp_version.py ``` -A simple operation stays in one operation or existing domain module. Do not -split it for symmetry. +A simple operation stays in one operation or domain module. Do not split it for +symmetry. When a complex operation has cohesive phases with distinct invariants, failure behavior, rollback, or tests, use: @@ -167,8 +162,8 @@ src/specify_cli/ └── mcp_init.py ``` -`_operation_init.py` is the application entry point. Phase modules do not -register commands or tools. +`_operation_init.py` is the shared application entry point. Phase modules do +not register commands or tools. Adapter-only phases retain adapter-specific names: @@ -177,8 +172,8 @@ _command__.py _mcp__.py ``` -If more than one adapter needs a phase, it is not adapter-private and belongs -in the shared operation or domain layer. +If more than one adapter needs a phase, it belongs in the shared operation or +domain layer. Nested directories continue to represent real command namespaces or bounded subdomains. Do not create an operation-phase directory that implies a @@ -195,42 +190,55 @@ The relevant command/domain hierarchy owns a typed request model. It: - Carries explicit consent such as `force` or external-source trust only when the operation defines that behavior. -Adapters perform transport parsing and map into this request. They do not -perform state-dependent semantic work while constructing it. - -## Validation and authorization lifecycle - -No denied capability may be exercised while deciding whether a request is -authorized. Invocation follows this order: - -1. The adapter parses and schema-validates input without filesystem, network, - environment, or process access. -2. The shared operation performs capability-free request validation using - only request values and static operation metadata. -3. The shared operation computes request-required capabilities, network - requirements, and requested roots without I/O. -4. The applicable policy layer authorizes those requirements. Under a - root-bound filesystem scope, it also performs a preliminary allowed-root - check on the unresolved requested path. A denial stops the invocation. -5. Under authorized `local-read`, the shared operation resolves the canonical - project or target root. A root-bound scope re-checks containment before - state-dependent validation. -6. The shared operation performs every filesystem access through the - authorized filesystem interface. -7. The shared operation performs its side effects and returns its typed - outcome. - -Capability-free validation covers types, enums, mutually exclusive fields, -required combinations, and similar pure invariants. State-dependent -validation includes reading project files, resolving installed integrations, -consulting catalogs, inspecting host tools, and other I/O. - -The computed requirements must conservatively cover every path reachable from -the pure validated request. Canonical resolution can inspect the filesystem -and follow symlinks, so it must not occur before authorization. The -post-resolution containment check is an admission check, not continuing proof -of confinement. Stateful validation must not discover and exercise an -additional unauthorized capability. +Adapters parse their transport input and map it into this request. Semantic +validation remains in the shared operation. + +## Invocation lifecycle + +Invocation follows a small, explicit sequence: + +1. The adapter parses and schema-validates its input. +2. The shared operation performs pure request validation and computes any + request-specific capabilities or network requirements. +3. An adapter that enforces policy, such as MCP, authorizes those requirements + before invoking behavior that uses them. +4. The shared operation performs state-dependent validation, orchestration, and + side effects. +5. The operation returns its typed outcome for adapter-specific reporting. + +The CLI normally proceeds under the invoking user's operating-system +permissions. MCP applies its configured policy before dispatch. Policy +differences may reject an invocation, but they do not create a second +implementation of the operation. + +## Project and working-directory handling + +Project-scoped request types carry an explicit project directory, or an +explicit value derived by the adapter from its launch working directory: + +- CLI defaults to the cwd captured when the command invocation begins. +- Local stdio MCP defaults to the cwd captured when the server starts. +- A caller may supply a project or target directory when the command supports + it. + +The shared operation or focused domain helper resolves and validates the path, +including `.specify` project checks where required. It passes the resolved path +through domain calls and operation phases. + +Shared code must not call `os.chdir()` to establish request state. A long-lived +MCP server may handle calls for different projects, and process-wide cwd would +couple otherwise independent invocations. + +Package metadata and first-party bundled assets use normal shared Python/domain +helpers such as `importlib.metadata`, `importlib.resources`, or the established +asset resolver. They are not caller-selected project paths and require no +universal application-resource abstraction. + +This architecture does not claim that an in-process path check is an operating +system sandbox. CLI and local stdio MCP run with the permissions of their +process user. A deployment that requires filesystem confinement must sandbox +the MCP server process or transport host; command behavior does not implement a +second virtual filesystem. ## Typed outcome contract @@ -238,7 +246,7 @@ The operation returns a typed outcome containing: - The command-specific result. - Zero or more structured warnings. -- Adapter-relevant execution metadata such as changed paths or transaction +- Command-relevant execution metadata such as changed paths or transaction status. Warnings have a stable code, human-readable message, and typed details. The @@ -294,161 +302,63 @@ Contract evolution follows these rules: - Tests lock established adapter schemas and machine-output shapes to the declared contract. -## Invocation context - -Adapters construct one immutable invocation context: - -```text -InvocationContext -├── launch_working_directory -├── requested_directory -├── filesystem_scope -├── filesystem: FilesystemAccess -├── application_resources: ReadOnlyApplicationResources -├── access_policy -├── deadline -├── cancellation -└── output_budget -``` - -`requested_directory` is the caller-supplied value, or absent when discovery -should begin from `launch_working_directory`. Constructing the context does not -resolve or validate a project. - -`filesystem_scope` is explicit: - -```text -RootBound(allowed_roots) -HostUser -``` - -`RootBound` confines access to host-provided roots and supplies a -`RootedFilesystem`. `HostUser` supplies a `HostFilesystem` governed by the -invoking user's operating-system permissions. Direct CLI and local stdio MCP -use `HostUser` by default. A host may deliberately configure `RootBound`; an -adapter must never infer it from cwd or silently switch scopes. - -`application_resources` is bound during trusted adapter/process composition to -the running Specify distribution, never from request data. - -The parsing and capability-computation phases do not call `filesystem` or -`application_resources`. After policy authorization, the shared operation uses -those interfaces to resolve and validate the canonical project or target root, -then passes the resolved root explicitly through operation phases. -`RootBound` re-checks canonical containment before state-dependent validation -or side effects. A raw canonical path is identity, not authorization under a -root-bound policy. - -Shared operations must not call `os.chdir()` to establish request context. -They pass resolved roots through operation phases and domain calls. Deadlines, -cancellation, and output budgets are likewise explicit. - -## Filesystem confinement - -Under `RootBound`, root validation alone does not confine later access. A -descendant can be a symlink or junction to an outside path, and a path -component can be replaced between validation and use. - -Every filesystem access made under a confined policy must therefore use -`RootedFilesystem` or an operation-owned equivalent that enforces the same -invariants: - -- Anchor traversal at an already-authorized root. -- Validate each descendant component without following unauthorized symlinks, - junctions, mount redirections, or equivalent platform indirections. -- Couple validation and use through descriptor-relative or handle-relative - access where the platform supports it. -- Re-check containment at the point of access when handle-relative traversal - is unavailable, reject indirections before and after creation, and fail - closed when the platform cannot enforce the boundary safely. -- Apply the boundary to reads, writes, creates, deletes, renames, temporary - files, archives, caches, and rollback paths. - -Command/domain code must not bypass the boundary with raw `Path`, `open`, or -unscoped filesystem helpers. Shared infrastructure may provide these -root-bound primitives, but command-specific path semantics remain owned by the -relevant hierarchy. - -`HostUser` is an explicit adapter policy, not a confinement mechanism. It does -not imply force, overwrite consent, external-source trust, or permission for a -different adapter to use host-wide access. - -## Trusted application resources - -Installed package metadata and first-party bundled assets are a separate -read-only authority domain from caller-selected project paths. They may live -outside an MCP server's `RootBound` roots in wheel, pipx, source-checkout, and -editable installations. - -After `local-read` authorization, operations may use -`ReadOnlyApplicationResources`. The interface: - -- Reads distribution metadata and validated first-party bundled resources by - logical identifier, not arbitrary caller-supplied path. -- Restricts backing locations to the running Specify distribution and its - validated source-checkout resource roots. -- Exposes no create, update, delete, rename, or arbitrary path traversal API. -- Rejects malformed or unknown identifiers and fails closed when a resource - cannot be validated. -- Keeps downloaded catalogs, third-party extensions, user configuration, and - project files outside this trusted resource domain. - -Operations must not add package installation paths to request-writable allowed -roots merely to read application assets. For example, `version` reads -distribution metadata through `application_resources`; `init` reads bundled -templates and scripts through that interface and writes them through -`filesystem`. - -Both adapters provide the application-resource interface explicitly. Shared -operations do not discover package roots from cwd, environment variables, or -caller input. - ## Capability declarations -Capabilities are independent requirements, not a highest-risk hierarchy: +Capabilities are cumulative operation metadata: | Capability | Meaning | | --- | --- | -| `local-read` | Read process, application, host, or project state permitted by the selected filesystem scope | -| `project-write` | Create or change files or configuration permitted by the selected filesystem scope | -| `execution` | Start host tools, workflows, hooks, agents, or processes | -| `unrestricted-host-execution` | Permit an executed child to use the server user's ambient host access outside configured confinement | -| `self-modifying` | Change the Specify installation or machine-level state | +| `local-read` | Reads local process, installation, host, or project state | +| `project-write` | Creates or changes project or target files/configuration | +| `execution` | Starts host tools, workflows, hooks, agents, or processes | +| `self-modifying` | Changes the Specify installation or machine-level state | The descriptor declares the conservative union an operation may require. `required_capabilities(request)` may compute an exact subset only from the -capability-free validated request and static metadata. +validated request and static metadata, without performing I/O. + +`execution` means the operation may start a child process with the MCP server +process user's privileges. It is not a filesystem sandbox. A host that needs +stronger isolation runs the server inside an appropriate OS sandbox, container, +or restricted account. + +Network access is declared separately as `none`, `optional`, or `required`. +Trust and destructive consent remain explicit request values, not implied +capabilities. -Resolving or validating a project or target root requires `local-read`, even -when the operation's eventual side effect is `project-write`. +## Trust and consent -`execution` does not waive filesystem confinement. An operation that starts a -child process must either: +The shared operation owns semantic rules that require explicit request values: -- Run it inside an enforceable sandbox limited to authorized roots, network, - environment, and subprocess behavior; or -- Declare `unrestricted-host-execution` in addition to `execution`. +- Trusting an external URL or downloaded executable content. +- Overwriting a non-empty target or user-modified file. +- Selecting a workflow, hook, installer, or other executable operation and + supplying any confirmation fields that operation defines. +- Performing a self-modifying action. -The second capability is an explicit acknowledgement that the child runs with -the server user's ambient privileges and can access paths outside -a `RootBound` scope. It is never implied by `execution`, project cwd, machine -mode, or transport authentication. If sandboxing is required but unavailable, -the operation fails with a structured policy error; it must not silently fall -back to unrestricted execution. +The CLI may prompt before constructing or retrying a request. MCP never prompts +and returns a structured input- or confirmation-required error when the +request lacks required consent. -When `unrestricted-host-execution` is authorized, filesystem, environment, -network, and subprocess restrictions cannot be claimed as enforced inside the -child. Policy must present the grant as that broad exception. Operation -descriptors still declare their intended managed network behavior, but an -arbitrary unsandboxed child is not a network-confined execution mode. +Machine-readable mode, non-interactive mode, transport authentication, or an +authorized capability never implies `force`, trust, or destructive consent. -Network access is declared separately as `none`, `optional`, or `required`. -Trust and destructive consent remain explicit request values, not implied -capabilities. +## Timeouts, cancellation, and bounded output + +Do not force every operation through a universal runtime object. + +- The MCP adapter owns protocol deadlines, cancellation, and response-size + enforcement. +- A command that can cooperatively cancel or accept a deadline exposes a + focused typed parameter or operation dependency for that behavior. +- Subprocess and network helpers receive explicit timeouts from the operation + that invokes them. +- Potentially large commands own pagination or limit fields in their request + and result contracts. +- Truncation is explicit and never returned as a successful complete result. -The MCP design defines policy enforcement for its tool surface. CLI invocation -and reporting remain defined by the CLI design, but the CLI adapter must not -change the operation's capability, trust, or consent semantics. +CLI and MCP adapters may choose different presentation limits, but neither may +change the semantic result silently. ## Testing structure @@ -470,24 +380,16 @@ Operation tests cover: - Valid requests and intended results. - Pure and state-dependent validation failures. - Warnings and structured errors. -- Capabilities, trust, consent, network behavior, side effects, rollback, - cancellation, and output bounds. -- Under `RootBound`, preliminary requested-root authorization and canonical - post-resolution containment, including symlink-escape rejection. -- Root-bound enforcement at each descendant filesystem access, including - symlink/junction replacement and time-of-check/time-of-use cases. -- Read-only application resources in wheel/pipx and source/editable layouts, - including rejection of caller-controlled paths and write attempts. -- Sandboxed execution and explicit `unrestricted-host-execution` policy - denial, with no unsafe fallback. +- Side effects, rollback, trust, consent, network behavior, and execution. +- Explicit project/target paths without process-wide cwd changes. - Domain behavior without Typer, Rich, MCP, or transport assertions. -Adapter tests cover invocation mapping, explicit filesystem scope and access -policy construction, and adapter-specific reporting. +Adapter tests cover invocation mapping and adapter-specific reporting and +policy. -Parity tests invoke CLI and MCP adapters against the same operation and policy -fixtures and compare semantic request, result, warning, error, and side-effect -behavior. Parity does not require byte-identical presentation. +Parity tests invoke CLI and MCP adapters against the same operation fixture and +compare semantic request, result, warning, error, and side-effect behavior. +Parity does not require byte-identical presentation. Behavioral changes follow [Testing deterministic behavior](../CONTRIBUTING.md#testing-deterministic-behavior): @@ -504,12 +406,10 @@ Avoid: - Duplicating validation or orchestration in adapters. - Adding adapter concepts to request, outcome, warning, or error models. - Hiding operations behind a central string dispatcher or service locator. +- Adding a universal invocation context, filesystem abstraction, or resource + provider when focused Python parameters and existing helpers suffice. - Letting adapters infer force, trust, consent, or extra capabilities. -- Leaving filesystem scope or default access policy implicit so shared code - must guess the adapter's authority. -- Adding installed package roots to project-writable roots instead of using - the read-only application-resource interface. -- Reading mutable process cwd instead of using invocation context. +- Reading mutable process cwd instead of passing an explicit path. - Splitting simple operations or creating phase modules solely for symmetry. ## Review checklist @@ -522,17 +422,10 @@ For an operation with CLI and MCP adapters: - [ ] Adapter modules contain only invocation, mapping, presentation, and adapter-specific concerns. - [ ] Shared request, outcome, warning, and error types are transport-neutral. -- [ ] Each adapter explicitly constructs its filesystem scope and access - policy. -- [ ] Trusted package metadata and bundled assets use - `ReadOnlyApplicationResources`, not project filesystem authority. -- [ ] Capability computation is pure and authorization precedes stateful work. -- [ ] Under `RootBound`, canonical project resolution and its allowed-root - re-check occur only after authorization. -- [ ] Every confined filesystem access remains anchored to authorized roots at - the point of use. -- [ ] Child processes are sandboxed or require explicit - `unrestricted-host-execution`. +- [ ] Project and target paths are explicit; shared code does not call + `os.chdir()`. +- [ ] MCP policy uses operation metadata without moving policy into shared + domain behavior. - [ ] CLI exit codes and MCP tool errors remain adapter-owned. - [ ] Contract-version ownership and compatibility tests are explicit. - [ ] Operation tests and adapter parity tests cover positive and negative From 6a31e51130003ce81504ade6478d12e3d71ac861 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:32:32 -0500 Subject: [PATCH 12/19] docs: simplify MCP access model Remove the internal MCP policy-engine design and keep side-effect and network declarations as host-facing metadata around the shared operation layer. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- design/mcp.md | 149 +++++++++++++++++------------------------------ design/shared.md | 39 ++++++------- 3 files changed, 74 insertions(+), 116 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d976a41905..3b36aba797 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ groups, registration ownership, and mirrored tests. Before adding or changing MCP tools for Specify commands, read [Shared Command Application Architecture](design/shared.md) and [Specify MCP Command Architecture](design/mcp.md). They define the shared -operation boundary, typed contracts, explicit inventory, access policy, +operation boundary, typed contracts, explicit inventory, side-effect metadata, transport separation, and mirrored tests. ## Adding or Updating Agent Integrations diff --git a/design/mcp.md b/design/mcp.md index 2abc258bf6..8e03eaf0cd 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -6,8 +6,8 @@ the Model Context Protocol (MCP). It is the MCP counterpart to Both adapters invoke the application layer defined by [Shared Command Application Architecture](shared.md). This document owns MCP -tool identity, exposure, policy, protocol mapping, and transport. It does not -redefine semantic validation, orchestration, results, warnings, errors, or +tool identity, exposure, annotations, protocol mapping, and transport. It does +not redefine semantic validation, orchestration, results, warnings, errors, or side effects. ## Design goals @@ -27,8 +27,8 @@ The design optimizes for: disposition; tools are never exposed through filesystem discovery. - **Small working context:** changing one operation should normally require only its domain, CLI adapter, MCP adapter, and mirrored tests. -- **Policy visibility:** project writes, execution, network access, trust - decisions, and self-modification are declared and enforced. +- **Side-effect visibility:** project writes, execution, network access, trust + decisions, and self-modification are declared for MCP hosts and clients. - **Transport independence:** stdio and future Streamable HTTP hosting do not change command behavior. @@ -61,15 +61,14 @@ The MCP adapter owns: - Tool name, description, annotations, and protocol schemas. - Mapping between MCP content and shared request/outcome types. - Per-group MCP registration and static inventory. -- Access-policy enforcement for MCP invocation. - Protocol diagnostics and transport hosting. It does not own semantic validation, application orchestration, side effects, or command-specific domain contracts. It must not invoke Typer handlers, start `specify` as application dispatch, scrape Rich output, or parse CLI stderr. -Shared MCP infrastructure may define policy and protocol primitives. It must -not become a central catalog of command-specific request models, results, +Shared MCP infrastructure may define protocol and registration primitives. It +must not become a central catalog of command-specific request models, results, validation, or orchestration. ## Operation identity and MCP tool design @@ -107,12 +106,12 @@ typed callable. First-class tools therefore preserve: - Per-command schemas and descriptions. - MCP client discovery and argument validation. - Command-specific output schemas and annotations. -- Reviewable registration and policy metadata. +- Reviewable registration and side-effect metadata. - A direct mapping back to the CLI leaf and owning source files. A generic facade would instead reduce the protocol-visible input to a command string plus an opaque or oversized union of arguments. That weakens schema -validation, discoverability, policy review, and compatibility analysis. +validation, discoverability, side-effect review, and compatibility analysis. At the time this document was written, the CLI had 90 leaf commands. `specify mcp` is the transport host and is permanently excluded from recursive @@ -246,22 +245,6 @@ The static disposition values are: current distribution or platform; the record states the concrete reason. - `excluded`: the command is intentionally not an MCP operation. -Runtime policy state is separate from static inventory disposition. An -available tool has a request-specific `effective_state` of: - -- `enabled`: the active policy authorizes the complete validated request, - including required capabilities, network access, and external-source trust - policy. -- `policy-disabled`: the tool remains discoverable, but invocation returns a - structured `policy_denied` error identifying every denied policy dimension. - -`effective_state` is derived only after capability-free request validation and -the full request policy decision; it is not stored as the inventory's static -disposition. A metadata or describe surface without a concrete request reports -the static disposition and active policy constraints, not a fabricated -`effective_state`. A request-evaluation surface may report both fields but must -preserve their distinct types. - Every CLI leaf must appear exactly once. The inventory parity test fails for a missing leaf, duplicate logical operation, duplicate tool name, stale CLI path, or unexplained exclusion. @@ -272,16 +255,14 @@ path, or unexplained exclusion. MCP tool that starts another MCP server would be recursive infrastructure, not an application operation. -Other commands are not excluded merely because they mutate state. They are -declared and gated by capability. For example: +Other commands are not excluded merely because they mutate state. Their +side effects are declared so MCP hosts and clients can make informed exposure +and confirmation decisions. For example: - `self.upgrade` has static disposition `available` when its first-class tool - is implemented. Its effective state is `policy-disabled` under the default - policy because local reads, execution, and self-modification are not all - authorized. + is implemented and declares local reads, execution, and self-modification. - `event.run`, `workflow.run`, and `workflow.resume` are execution operations - that may also persist project state; policy must authorize every capability - required by the operation. + that may also persist project state. - `init`, add/remove/update commands, and configuration changes are project-write operations, with execution and other independent capabilities declared when their paths require them. @@ -302,8 +283,8 @@ The MCP adapter projects that contract onto MCP: - Its input schema is command-specific and maps into the shared typed request. - It performs protocol/schema validation but no state-dependent semantic work. -- It applies MCP policy to the shared operation's declared requirements before - invoking application behavior. +- It exposes the operation's side-effect and network metadata through tool + annotations and inventory. - It maps the shared result and warnings into command-specific structured content. - It maps expected shared errors into MCP tool errors without adding @@ -328,8 +309,6 @@ The adapter and shared operation follow these rules: - Do not call `os.chdir()` for an MCP request. A long-lived server may process concurrent or sequential calls with different project contexts. -- Authorize the operation's declared capabilities and network requirements - before invoking behavior that uses them. - Pass the resolved project or target path explicitly through operation phases. - Use the same shared Python/domain helpers as CLI for distribution metadata, bundled assets, project files, and other application behavior. @@ -366,16 +345,15 @@ Machine mode is not consent. Starting MCP, using `--json` or - Permission to execute behavior that the caller did not explicitly request. The caller must explicitly invoke an execution operation, supply any -command-specific consent fields, and satisfy the active access policy. MCP -annotations and host UI are advisory; the server still enforces operation -requirements. +command-specific consent fields, and satisfy the shared operation's semantic +validation. MCP annotations and host UI are advisory; they do not substitute +for required request values. -## Capability requirements and access policy +## Capability and network metadata -Operations declare and compute capabilities according to -[the shared capability contract](shared.md#capability-declarations). MCP policy -must authorize every request-required capability; choosing one "highest" class -is not sufficient. Examples: +Operations declare capabilities according to +[the shared capability contract](shared.md#capability-declarations). These are +cumulative descriptions, not a highest-risk hierarchy. Examples: ```text version -> {local-read} @@ -384,7 +362,7 @@ workflow.run -> {local-read, project-write, execution} self.upgrade -> {local-read, execution, self-modifying} ``` -`read-only` is a derived description, not an authorizable capability. A +`read-only` is a derived description, not a declared capability. A request is read-only only when it requires no `project-write`, `execution`, or `self-modifying` capability. An operation that launches a binary is therefore not read-only even if it does not persist changes. @@ -393,30 +371,18 @@ Network access is an independent declaration: `none`, `optional`, or `required`. A read-only search may use the network, while a project-write operation may be fully offline. -The MCP server receives an access policy from its host configuration. The -default policy is conservative: - -- `local-read` is authorized. -- `project-write`, `execution`, and `self-modifying` require explicit - authorization. -- Network access is denied unless explicitly enabled. -- External-source trust is separately controlled and remains default-deny. - -Tool annotations should conservatively reflect the full declared capability -set, but annotations do not replace server-side enforcement. If policy denies -any capability required by an otherwise implemented tool request, the -registered tool returns a structured `policy_denied` error identifying the -denied policy dimensions. A request-evaluation surface may report -`effective_state: policy-disabled`; the static inventory disposition remains -`available`. - -An operation must not omit a capability merely because the path is rare, -optional, expected to be idempotent, or combined with a more powerful -capability. MCP enforces the shared operation's static and request-specific -declarations; it does not infer or reduce them independently. - -`execution` authorizes an operation to launch a child process. That child runs -with the MCP server process user's privileges unless the host externally +Tool annotations and inventory conservatively reflect the operation's declared +capabilities and network access. The stdio server does not implement an +allow/deny policy engine or request-specific availability state. The MCP host +or client may use metadata to hide a tool, ask for confirmation, or decline +to invoke it. + +The server still enforces semantic request requirements such as `force`, +external-source trust, and command-specific confirmation fields because those +belong to the shared operation contract. + +`execution` declares that an operation may launch a child process. That child +runs with the MCP server process user's privileges unless the host externally sandboxes the server. Setting cwd inside a project is not a security boundary, and this command architecture does not claim otherwise. @@ -430,10 +396,10 @@ confirmation. The adapters own how explicit consent enters the request. - A non-empty target directory remains protected without explicit overwrite consent. - Catalog discovery permission does not imply install permission. -- A network-enabled policy does not imply trust in arbitrary returned content. +- Network availability does not imply trust in arbitrary returned content. - Redirect, digest, source, and compatibility validation remain domain behavior, shared by both adapters. -- Transport authentication does not replace operation authorization. +- Transport authentication does not replace semantic consent. Network calls use bounded connect/read timeouts. Operations do not silently switch from offline to online behavior. When a request supports offline @@ -486,7 +452,6 @@ src/specify_cli/mcp_server/ ├── __init__.py ├── server.py ├── registry.py -├── policy.py └── transports/ ├── stdio.py └── streamable_http.py @@ -506,11 +471,10 @@ Shared operation, CLI adapter, and parity coverage follows - Verify tool name, description, annotations, and exact input/output schemas. - Verify mapping to the shared operation request and outcome. - Verify structured warnings and tool errors. -- Verify access-policy, trust, timeout, cancellation, and output-budget - failures. +- Verify trust, timeout, cancellation, and output-budget failures. - Verify project-directory mapping and operation dispatch without `os.chdir()`. -- Verify execution and network operations are denied when their declared - requirements are not authorized. +- Verify tool annotations accurately expose declared capabilities and network + access. ### Inventory tests @@ -519,9 +483,8 @@ Shared operation, CLI adapter, and parity coverage follows - Reject duplicate operation IDs and MCP tool names. - Require reasons for every unavailable or excluded command. - Verify available tools are registered by the owning hierarchy. -- Verify request-specific `effective_state` uses the complete policy decision, - including a network-required request whose capabilities are authorized but - whose network access is denied. +- Verify inventory capability and network metadata match the shared operation + descriptors. - Preserve total pytest collection when tests move, as required by the CLI architecture. @@ -531,8 +494,8 @@ Shared operation, CLI adapter, and parity coverage follows - Keep a real stdio initialize/list/call test with protocol-pure stdout. - Add equivalent Streamable HTTP protocol, authentication, cancellation, and isolation tests when that transport exists. -- Test malformed input, policy denial, unavailable tools, internal failure - sanitization, and output bounds as negative cases. +- Test malformed input, unavailable tools, internal failure sanitization, and + output bounds as negative cases. Behavioral changes follow [Testing deterministic behavior](../CONTRIBUTING.md#testing-deterministic-behavior): @@ -599,12 +562,12 @@ network_access: none project_scope: required ``` -The request contains an optional project directory. After authorization, the -shared operation resolves and validates that path, uses `ArtifactCatalog`, and -returns typed artifact rows. The CLI adapter preserves its JSON stream -contract; the MCP adapter exposes the rows through its output schema and never -captures CLI stdout. Large results use explicit operation limits or pagination -rather than silent truncation. +The request contains an optional project directory. The shared operation +resolves and validates that path, uses `ArtifactCatalog`, and returns typed +artifact rows. The CLI adapter preserves its JSON stream contract; the MCP +adapter exposes the rows through its output schema and never captures CLI +stdout. Large results use explicit operation limits or pagination rather than +silent truncation. ### `init`: complex project mutation @@ -653,7 +616,7 @@ Bundled templates and scripts use the same shared asset helpers as CLI. If the target is non-empty and `force` is false, both adapters receive the same semantic confirmation-required failure. The CLI may respond by prompting and retrying with explicit consent; the MCP tool returns the structured error and -requires a new call with `force=true`, subject to policy. +requires a new call with `force=true`. ## Anti-patterns @@ -668,7 +631,8 @@ Avoid: - Hiding behavior behind a central service locator or string-based dispatcher. - Exposing every operation through one generic run tool. - Forcing every command into an oversized universal execution engine. -- Treating MCP tool annotations as authorization. +- Treating MCP tool annotations or host confirmation as semantic `force`, + trust, or destructive consent. - Treating machine mode as force, trust, overwrite consent, or execution permission. - Reading stdin or changing process-wide cwd during a tool call. @@ -691,11 +655,8 @@ For a new or migrated MCP operation: - [ ] The MCP tool is first-class and has a command-specific schema. - [ ] The tool name and source layout mirror the CLI path. - [ ] The owning command hierarchy declares registration and inventory. -- [ ] Availability, possible and request-required capabilities, network - access, project scope, and - contract version are explicit. -- [ ] Request-specific `effective_state` reflects the complete policy decision, - not capabilities alone. +- [ ] Availability, capabilities, network access, project scope, and contract + version are explicit. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. - [ ] Timeouts, cancellation, stdin, and output bounds are handled. diff --git a/design/shared.md b/design/shared.md index 25433c6acb..ccb47946a0 100644 --- a/design/shared.md +++ b/design/shared.md @@ -6,8 +6,8 @@ delivery surface translate their inputs into that operation and translate its outcome into their own output and error conventions. [Specify CLI Command Architecture](cli.md) defines the Typer/terminal adapter. -[Specify MCP Command Architecture](mcp.md) defines MCP tool exposure, policy, -and transport. Neither adapter document owns application behavior. +[Specify MCP Command Architecture](mcp.md) defines MCP tool exposure, protocol +mapping, and transport. Neither adapter document owns application behavior. ## Design goals @@ -35,7 +35,7 @@ The shared layer does not: - Require byte-identical CLI JSON and MCP protocol envelopes. - Turn operations into a universal string-based command dispatcher. - Replace focused domain modules with a central execution engine. -- Define CLI prompting, MCP authorization, transport authentication, or host +- Define CLI prompting, MCP host approval, transport authentication, or host sandboxing. - Require a context object or extra module when ordinary typed parameters and an existing domain module are sufficient. @@ -76,7 +76,7 @@ An adapter must not: | Project and target path validation | Shared operation or focused domain helper | | Bundled assets and distribution metadata | Existing shared/domain resource helpers | | CLI syntax, prompting, rendering, JSON, and exit codes | CLI adapter | -| MCP schemas, policy enforcement, annotations, and tool errors | MCP adapter | +| MCP schemas, annotations, protocol mapping, and tool errors | MCP adapter | | MCP server lifecycle and transport | MCP infrastructure | Domain behavior that already has a focused Typer-free owner remains there. A @@ -198,18 +198,15 @@ validation remains in the shared operation. Invocation follows a small, explicit sequence: 1. The adapter parses and schema-validates its input. -2. The shared operation performs pure request validation and computes any - request-specific capabilities or network requirements. -3. An adapter that enforces policy, such as MCP, authorizes those requirements - before invoking behavior that uses them. -4. The shared operation performs state-dependent validation, orchestration, and +2. The shared operation performs pure request validation. +3. The shared operation performs state-dependent validation, orchestration, and side effects. -5. The operation returns its typed outcome for adapter-specific reporting. +4. The operation returns its typed outcome for adapter-specific reporting. The CLI normally proceeds under the invoking user's operating-system -permissions. MCP applies its configured policy before dispatch. Policy -differences may reject an invocation, but they do not create a second -implementation of the operation. +permissions. MCP publishes operation metadata so its host or client can decide +whether to expose, confirm, or invoke a tool. Once invoked, both adapters call +the same application implementation. ## Project and working-directory handling @@ -314,8 +311,8 @@ Capabilities are cumulative operation metadata: | `self-modifying` | Changes the Specify installation or machine-level state | The descriptor declares the conservative union an operation may require. -`required_capabilities(request)` may compute an exact subset only from the -validated request and static metadata, without performing I/O. +Adapters and hosts use this metadata for discovery, review, and confirmation; +it does not add a policy engine to the shared layer. `execution` means the operation may start a child process with the MCP server process user's privileges. It is not a filesystem sandbox. A host that needs @@ -340,8 +337,8 @@ The CLI may prompt before constructing or retrying a request. MCP never prompts and returns a structured input- or confirmation-required error when the request lacks required consent. -Machine-readable mode, non-interactive mode, transport authentication, or an -authorized capability never implies `force`, trust, or destructive consent. +Machine-readable mode, non-interactive mode, transport authentication, or a +host confirmation never implies `force`, trust, or destructive consent. ## Timeouts, cancellation, and bounded output @@ -384,8 +381,8 @@ Operation tests cover: - Explicit project/target paths without process-wide cwd changes. - Domain behavior without Typer, Rich, MCP, or transport assertions. -Adapter tests cover invocation mapping and adapter-specific reporting and -policy. +Adapter tests cover invocation mapping, metadata, and adapter-specific +reporting. Parity tests invoke CLI and MCP adapters against the same operation fixture and compare semantic request, result, warning, error, and side-effect behavior. @@ -424,8 +421,8 @@ For an operation with CLI and MCP adapters: - [ ] Shared request, outcome, warning, and error types are transport-neutral. - [ ] Project and target paths are explicit; shared code does not call `os.chdir()`. -- [ ] MCP policy uses operation metadata without moving policy into shared - domain behavior. +- [ ] MCP annotations and inventory reflect operation metadata without moving + host approval behavior into the shared layer. - [ ] CLI exit codes and MCP tool errors remain adapter-owned. - [ ] Contract-version ownership and compatibility tests are explicit. - [ ] Operation tests and adapter parity tests cover positive and negative From d9db71e9fb147db42be8697f696dc3e2b8941f3e Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:15:38 -0500 Subject: [PATCH 13/19] docs: remove MCP leaf counts Keep the design focused on the durable inventory invariant rather than point-in-time command totals. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index 8e03eaf0cd..96c727f7c4 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -113,13 +113,8 @@ A generic facade would instead reduce the protocol-visible input to a command string plus an opaque or oversized union of arguments. That weakens schema validation, discoverability, side-effect review, and compatibility analysis. -At the time this document was written, the CLI had 90 leaf commands. -`specify mcp` is the transport host and is permanently excluded from recursive -exposure, leaving 89 leaf operations that require an explicit inventory -disposition. This count is large enough that registration must be organized -per command hierarchy, but not a reason to erase command-specific contracts -behind a generic tool. Tests should derive the current count rather than -hard-code 90. +Registration is organized per command hierarchy so command-specific contracts +remain reviewable without being hidden behind a generic tool. ### Command inventory From 531e0fc5dab8c0a39e45a0836e0aed31e8416fa2 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:13:19 -0500 Subject: [PATCH 14/19] docs: keep MCP architecture local Remove speculative remote transports and leftover universal descriptor fields while defining the conservative standard MCP annotation mapping. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- design/mcp.md | 111 +++++++++++++++++++++++++---------------------- design/shared.md | 10 ++--- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3b36aba797..949f4999f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Before adding or changing MCP tools for Specify commands, read [Shared Command Application Architecture](design/shared.md) and [Specify MCP Command Architecture](design/mcp.md). They define the shared operation boundary, typed contracts, explicit inventory, side-effect metadata, -transport separation, and mirrored tests. +the local stdio protocol boundary, and mirrored tests. ## Adding or Updating Agent Integrations diff --git a/design/mcp.md b/design/mcp.md index 96c727f7c4..5fdc2312f3 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -6,9 +6,9 @@ the Model Context Protocol (MCP). It is the MCP counterpart to Both adapters invoke the application layer defined by [Shared Command Application Architecture](shared.md). This document owns MCP -tool identity, exposure, annotations, protocol mapping, and transport. It does -not redefine semantic validation, orchestration, results, warnings, errors, or -side effects. +tool identity, exposure, annotations, protocol mapping, and local stdio +hosting. It does not redefine semantic validation, orchestration, results, +warnings, errors, or side effects. ## Design goals @@ -28,17 +28,17 @@ The design optimizes for: - **Small working context:** changing one operation should normally require only its domain, CLI adapter, MCP adapter, and mirrored tests. - **Side-effect visibility:** project writes, execution, network access, trust - decisions, and self-modification are declared for MCP hosts and clients. -- **Transport independence:** stdio and future Streamable HTTP hosting do not - change command behavior. + decisions, and self-modification are declared for review and conservatively + projected into standard MCP annotations. +- **Local stdio boundary:** protocol framing and diagnostics remain outside + command behavior. ## Non-goals This design does not: - Make MCP a wrapper around the human CLI. -- Require every CLI leaf to be remotely invokable regardless of risk or - readiness. +- Require every CLI leaf to be invokable regardless of risk or readiness. - Turn existing human output into an MCP or JSON contract. - Make `--json`, `--non-interactive`, or an MCP invocation imply `--force`, trust, destructive consent, or network permission. @@ -61,7 +61,7 @@ The MCP adapter owns: - Tool name, description, annotations, and protocol schemas. - Mapping between MCP content and shared request/outcome types. - Per-group MCP registration and static inventory. -- Protocol diagnostics and transport hosting. +- Protocol diagnostics and local stdio server hosting. It does not own semantic validation, application orchestration, side effects, or command-specific domain contracts. It must not invoke Typer handlers, start @@ -144,10 +144,10 @@ workflow.step.catalog: add, list, remove workflow.overlay: add, set-priority, enable, disable, remove, list ``` -`mcp` is excluded because it is the transport host. Every other available leaf -maps to a first-class tool using the naming rule above. A leaf that is -unavailable or excluded must still have an explicit hierarchy-owned inventory -record and reason. +`mcp` is excluded because it starts the local stdio server. Every other +available leaf maps to a first-class tool using the naming rule above. A leaf +that is unavailable or excluded must still have an explicit hierarchy-owned +inventory record and reason. This list documents the command namespaces; it is not a registration source. The hierarchy-owned inventory and its parity tests are authoritative. @@ -229,8 +229,6 @@ disposition disposition_reason capabilities network_access -project_scope -default_timeout ``` The static disposition values are: @@ -246,9 +244,9 @@ path, or unexplained exclusion. ### Permanent and conditional exclusions -`specify mcp` is permanently excluded because it hosts the MCP transport; an -MCP tool that starts another MCP server would be recursive infrastructure, not -an application operation. +`specify mcp` is permanently excluded because it starts the local stdio server; +an MCP tool that starts another MCP server would be recursive infrastructure, +not an application operation. Other commands are not excluded merely because they mutate state. Their side effects are declared so MCP hosts and clients can make informed exposure @@ -278,8 +276,9 @@ The MCP adapter projects that contract onto MCP: - Its input schema is command-specific and maps into the shared typed request. - It performs protocol/schema validation but no state-dependent semantic work. -- It exposes the operation's side-effect and network metadata through tool - annotations and inventory. +- It projects the operation's side-effect and network metadata into standard + MCP annotations while the hierarchy-owned inventory retains the exact + declarations. - It maps the shared result and warnings into command-specific structured content. - It maps expected shared errors into MCP tool errors without adding @@ -317,7 +316,7 @@ Local stdio runs with the operating-system permissions of the server process, just as the CLI runs with its process user's permissions. The command architecture does not claim to provide a per-operation filesystem sandbox. A host that needs confinement runs the MCP server inside an OS sandbox, container, -restricted account, or equivalent transport-host boundary. +or restricted account. ## Non-interactive behavior @@ -366,11 +365,30 @@ Network access is an independent declaration: `none`, `optional`, or `required`. A read-only search may use the network, while a project-write operation may be fully offline. -Tool annotations and inventory conservatively reflect the operation's declared -capabilities and network access. The stdio server does not implement an -allow/deny policy engine or request-specific availability state. The MCP host -or client may use metadata to hide a tool, ask for confirmation, or decline -to invoke it. +The hierarchy-owned inventory retains the exact capability set and network +state for review and parity tests. Standard MCP `ToolAnnotations` are hints, +not a lossless capability contract: + +- `readOnlyHint` is true only when the operation has no `project-write`, + `execution`, or `self-modifying` capability. +- `destructiveHint` is true when the operation may overwrite, delete, replace, + or reconfigure existing state. It is false only for additive updates. +- `idempotentHint` is true only when the operation contract guarantees that + repeated calls with the same arguments have no additional effect. +- `openWorldHint` is true when network access is `optional` or `required`, or + when execution may interact with external entities not bounded to the + process, installation, or selected project. + +The latter three hints depend on the full operation contract and are not +derived from the capability set alone. Exact capability names and the +three-state network declaration remain architecture and inventory metadata, +not protocol fields. This architecture does not require a custom `_meta` +contract or inventory tool. + +The stdio server does not implement an allow/deny policy engine or +request-specific availability state. An MCP host or client may use standard +annotations to inform visibility or confirmation, but the hints are not an +access-control boundary. The server still enforces semantic request requirements such as `force`, external-source trust, and command-specific confirmation fields because those @@ -403,7 +421,7 @@ as the CLI. ## Timeouts, cancellation, stdin, and bounded output -The transport adapter owns protocol deadlines, cancellation, and response-size +The stdio adapter owns protocol deadlines, cancellation, and response-size enforcement. Operations expose focused support only when their behavior can cooperate with it. @@ -422,23 +440,18 @@ cooperate with it. - MCP response-size enforcement belongs to the adapter; pagination semantics belong to the command hierarchy. -## Transport separation +## Local stdio server boundary -`specify_cli/mcp_server/` owns server composition and transport hosting, not -command behavior. +`specify_cli/mcp_server/` owns server composition and stdio protocol hosting, +not command behavior. -The initial transport remains stdio: +The MCP server runs locally over stdio because its operations act on the local +project, Specify installation, filesystem, and host tools: - Stdout is reserved for MCP protocol frames. - Logs and diagnostics use stderr or the SDK's logging channel. - Startup banners, Rich rendering, and CLI warnings never enter stdout. - -Future Streamable HTTP support should add a transport host around the same -tool registry and operation adapters. HTTP-specific authentication, sessions, -origin checks, request sizing, and connection cancellation belong to that -transport layer. Tool names, schemas, operation contracts, project behavior, -and capability requirements must not change merely because the transport -changes. +- The server uses the launch process user's local permissions. An illustrative infrastructure layout is: @@ -447,9 +460,7 @@ src/specify_cli/mcp_server/ ├── __init__.py ├── server.py ├── registry.py -└── transports/ - ├── stdio.py - └── streamable_http.py +└── stdio.py ``` Create only the modules justified by implemented behavior. The layout defines @@ -468,8 +479,8 @@ Shared operation, CLI adapter, and parity coverage follows - Verify structured warnings and tool errors. - Verify trust, timeout, cancellation, and output-budget failures. - Verify project-directory mapping and operation dispatch without `os.chdir()`. -- Verify tool annotations accurately expose declared capabilities and network - access. +- Verify standard tool annotations follow the conservative mapping and the + inventory retains exact capability and network declarations. ### Inventory tests @@ -487,8 +498,6 @@ Shared operation, CLI adapter, and parity coverage follows - Keep an in-memory MCP registration and dispatch test. - Keep a real stdio initialize/list/call test with protocol-pure stdout. -- Add equivalent Streamable HTTP protocol, authentication, cancellation, and - isolation tests when that transport exists. - Test malformed input, unavailable tools, internal failure sanitization, and output bounds as negative cases. @@ -521,7 +530,6 @@ cli_path: specify version mcp_tool_name: specify_version capabilities: [local-read] network_access: none -project_scope: process ``` `_operation_version.py` owns typed version collection and `VersionResult`, @@ -554,7 +562,6 @@ cli_path: specify artifact list mcp_tool_name: specify_artifact_list capabilities: [local-read] network_access: none -project_scope: required ``` The request contains an optional project directory. The shared operation @@ -597,7 +604,6 @@ cli_path: specify init mcp_tool_name: specify_init capabilities: [local-read, project-write, execution] network_access: optional -project_scope: creates-target ``` The request explicitly carries the target, integration, script type, optional @@ -639,7 +645,7 @@ Avoid: - Silently omitting CLI leaves from the MCP inventory. - Returning partial, truncated, or fallback data as a successful complete result. -- Letting transport concerns leak into command contracts. +- Letting stdio protocol concerns leak into command contracts. ## Review checklist @@ -650,13 +656,14 @@ For a new or migrated MCP operation: - [ ] The MCP tool is first-class and has a command-specific schema. - [ ] The tool name and source layout mirror the CLI path. - [ ] The owning command hierarchy declares registration and inventory. -- [ ] Availability, capabilities, network access, project scope, and contract - version are explicit. +- [ ] Availability, capabilities, network access, and contract version are + explicit. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. - [ ] Timeouts, cancellation, stdin, and output bounds are handled. - [ ] Existing CLI human and JSON behavior remains compatible. - [ ] Operation, CLI adapter, MCP adapter, parity, and protocol tests cover positive and negative behavior. -- [ ] Stdio remains protocol-pure, and command behavior is transport-neutral. +- [ ] Stdio remains protocol-pure, and command behavior stays outside server + hosting. - [ ] No command behavior was added to central MCP infrastructure. diff --git a/design/shared.md b/design/shared.md index ccb47946a0..62f3abed32 100644 --- a/design/shared.md +++ b/design/shared.md @@ -113,8 +113,6 @@ warning_types error_types capabilities network_access -project_scope -default_timeout ``` Adapter registration extends this metadata without moving command-specific @@ -234,8 +232,8 @@ universal application-resource abstraction. This architecture does not claim that an in-process path check is an operating system sandbox. CLI and local stdio MCP run with the permissions of their process user. A deployment that requires filesystem confinement must sandbox -the MCP server process or transport host; command behavior does not implement a -second virtual filesystem. +the MCP server process; command behavior does not implement a second virtual +filesystem. ## Typed outcome contract @@ -269,8 +267,8 @@ The operation hierarchy owns error codes and detail schemas. Adapters map them: - CLI maps them to human or JSON failure output and established exit codes. - MCP maps them to structured tool errors. -Shared errors contain no CLI exit code, Rich markup, MCP content block, HTTP -status, traceback, raw subprocess output, or secret. +Shared errors contain no CLI exit code, Rich markup, MCP content block, +traceback, raw subprocess output, or secret. Unexpected exceptions are normalized by the adapter boundary to a sanitized internal error and logged only through the adapter's diagnostic channel. From 2369c5554abc97bc73e83e0ef9ce3c3336dfc7c5 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:28:01 -0500 Subject: [PATCH 15/19] docs: clarify command contract metadata Keep operation contract versions internal to hierarchy inventories and tests, and scope shared-operation checklist requirements to multi-adapter commands. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 10 ++++++---- design/mcp.md | 9 +++++---- design/shared.md | 11 +++++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/design/cli.md b/design/cli.md index c7f92907c4..dd83f61f47 100644 --- a/design/cli.md +++ b/design/cli.md @@ -412,10 +412,12 @@ For a new or refactored command: - [ ] The CLI path maps predictably to a `command_.py` module. - [ ] Only the real command module registers a handler. -- [ ] The adapter maps into the shared operation defined by `design/shared.md`. -- [ ] Semantic validation, orchestration, and side effects are below the CLI - adapter. -- [ ] Project and target directories map explicitly into the shared request. +- [ ] If another adapter exposes the operation, the CLI adapter maps into the + shared operation defined by `design/shared.md`. +- [ ] For a multi-adapter operation, semantic validation, orchestration, and + side effects are below the CLI adapter. +- [ ] When a shared request exists, project and target directories map into it + explicitly. - [ ] CLI-private phase modules use `_command__.py`; shared phases use `_operation__.py`. - [ ] `_commands.py` contains only group infrastructure and genuinely shared diff --git a/design/mcp.md b/design/mcp.md index 5fdc2312f3..a97217bbe4 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -283,11 +283,12 @@ The MCP adapter projects that contract onto MCP: content. - It maps expected shared errors into MCP tool errors without adding success-shaped fallbacks. -- It exposes the operation's declared `contract_version` through tool or - inventory metadata. MCP protocol envelopes do not force a universal application result envelope. The command/domain hierarchy continues to own the semantic result shape. +The hierarchy-owned inventory records `contract_version` for compatibility and +parity tests, but the local MCP protocol does not add a custom version field to +tools or results. Unexpected exceptions become sanitized `internal_error` tool failures and are logged only through the MCP diagnostic channel. @@ -656,8 +657,8 @@ For a new or migrated MCP operation: - [ ] The MCP tool is first-class and has a command-specific schema. - [ ] The tool name and source layout mirror the CLI path. - [ ] The owning command hierarchy declares registration and inventory. -- [ ] Availability, capabilities, network access, and contract version are - explicit. +- [ ] Availability, capabilities, network access, and inventory contract + version are explicit. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. - [ ] Timeouts, cancellation, stdin, and output bounds are handled. diff --git a/design/shared.md b/design/shared.md index 62f3abed32..5a19c12ea8 100644 --- a/design/shared.md +++ b/design/shared.md @@ -279,12 +279,11 @@ The operation descriptor is the source of truth for `contract_version`. The version covers the semantic request, result, warning, and expected-error contract, not package or transport versions. -Both adapters conform to that declared version: - -- MCP exposes it through tool or inventory metadata. -- CLI contract tests reference it while preserving established human and JSON - output. A version field is not injected into an existing JSON result unless - that result contract already defines one. +Both adapters conform to that declared version. It is hierarchy-owned source +and inventory metadata used by adapter contract tests; it is not automatically +injected into CLI JSON or MCP tool metadata. A version field appears in a +machine result only when that command's established result contract defines +one. Contract evolution follows these rules: From 9581d3cfec753619e269d0f8a8d0386f6dbc6d81 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:42:23 -0500 Subject: [PATCH 16/19] docs: clarify MCP cancellation ownership Separate client-owned stdio cancellation from server-enforced operation timeouts and document the no-response cancellation behavior. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 22 ++++++++++++++-------- design/shared.md | 10 ++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index a97217bbe4..aa9a3bba97 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -422,17 +422,21 @@ as the CLI. ## Timeouts, cancellation, stdin, and bounded output -The stdio adapter owns protocol deadlines, cancellation, and response-size -enforcement. Operations expose focused support only when their behavior can +The MCP client owns its request deadline. If it no longer wants the result, it +sends `notifications/cancelled`. The stdio adapter handles that notification +and response-size enforcement; it does not create or own the client deadline. +Operations expose focused cancellation support only when their behavior can cooperate with it. -- Subprocesses and network calls receive explicit timeouts. -- A complex operation may accept a cancellation token or deadline as a focused - parameter; there is no universal invocation context. +- Subprocesses and network calls receive explicit server-side timeouts. +- A complex operation may accept a focused cancellation signal or + operation-owned timeout; there is no universal invocation context. - Transactional mutations roll back or report partial state according to their domain contract. -- Cancellation returns a structured cancellation error, never a successful - empty result. +- After client cancellation, the server stops work and sends no tool result or + tool error for that request. +- A server-enforced timeout while the request remains active returns a + structured timeout error, never a successful empty result. - No operation reads stdin or inherits an interactive child stdin. - Captured stdout/stderr and diagnostic details are size-bounded. - Potentially large lists use command-owned limits or pagination. @@ -478,7 +482,8 @@ Shared operation, CLI adapter, and parity coverage follows - Verify tool name, description, annotations, and exact input/output schemas. - Verify mapping to the shared operation request and outcome. - Verify structured warnings and tool errors. -- Verify trust, timeout, cancellation, and output-budget failures. +- Verify trust failures, server-enforced timeout errors, client cancellation + without a response, and output-budget failures. - Verify project-directory mapping and operation dispatch without `os.chdir()`. - Verify standard tool annotations follow the conservative mapping and the inventory retains exact capability and network declarations. @@ -499,6 +504,7 @@ Shared operation, CLI adapter, and parity coverage follows - Keep an in-memory MCP registration and dispatch test. - Keep a real stdio initialize/list/call test with protocol-pure stdout. +- Verify `notifications/cancelled` stops cooperative work without a response. - Test malformed input, unavailable tools, internal failure sanitization, and output bounds as negative cases. diff --git a/design/shared.md b/design/shared.md index 5a19c12ea8..b07b6e1220 100644 --- a/design/shared.md +++ b/design/shared.md @@ -341,12 +341,14 @@ host confirmation never implies `force`, trust, or destructive consent. Do not force every operation through a universal runtime object. -- The MCP adapter owns protocol deadlines, cancellation, and response-size - enforcement. -- A command that can cooperatively cancel or accept a deadline exposes a - focused typed parameter or operation dependency for that behavior. +- The MCP client owns its request deadline and may send a cancellation + notification. The stdio adapter handles that signal and enforces response + size limits. +- A command that can cooperatively stop exposes a focused cancellation + parameter or operation dependency for that behavior. - Subprocess and network helpers receive explicit timeouts from the operation that invokes them. +- A server-enforced operation timeout is distinct from client cancellation. - Potentially large commands own pagination or limit fields in their request and result contracts. - Truncation is explicit and never returned as a successful complete result. From 8571b4bdad6cca03628b11fe474fcc77a86bdd9f Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:56:06 -0500 Subject: [PATCH 17/19] docs: define read-only MCP annotations Specify that destructiveHint is omitted for read-only tools and is otherwise set from destructive versus additive behavior. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index aa9a3bba97..769ed79be9 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -372,8 +372,9 @@ not a lossless capability contract: - `readOnlyHint` is true only when the operation has no `project-write`, `execution`, or `self-modifying` capability. -- `destructiveHint` is true when the operation may overwrite, delete, replace, - or reconfigure existing state. It is false only for additive updates. +- `destructiveHint` is omitted when `readOnlyHint` is true. Otherwise it is + true when the operation may overwrite, delete, replace, or reconfigure + existing state, and false only for additive updates. - `idempotentHint` is true only when the operation contract guarantees that repeated calls with the same arguments have no additional effect. - `openWorldHint` is true when network access is `optional` or `required`, or From f25413f9c0c7d752a439bd3a184ea22d2dbe69d7 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:08:27 -0500 Subject: [PATCH 18/19] docs: require MCP inventory for CLI leaves Make every CLI leaf record an MCP disposition even when the operation remains unavailable or excluded. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/cli.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/design/cli.md b/design/cli.md index dd83f61f47..1cba8f9285 100644 --- a/design/cli.md +++ b/design/cli.md @@ -412,6 +412,8 @@ For a new or refactored command: - [ ] The CLI path maps predictably to a `command_.py` module. - [ ] Only the real command module registers a handler. +- [ ] The owning hierarchy records exactly one MCP inventory disposition for + the CLI leaf, including when it is unavailable or excluded. - [ ] If another adapter exposes the operation, the CLI adapter maps into the shared operation defined by `design/shared.md`. - [ ] For a multi-adapter operation, semantic validation, orchestration, and From ea4c338bb72399ad6965e7da8d6403d9d11c5246 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:15:52 -0500 Subject: [PATCH 19/19] docs: remove MCP cancellation contract Keep the local stdio architecture limited to explicit operation timeouts and bounded output rather than promising command-level cancellation. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- design/mcp.md | 21 +++++++-------------- design/shared.md | 9 ++------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/design/mcp.md b/design/mcp.md index 769ed79be9..89818d84db 100644 --- a/design/mcp.md +++ b/design/mcp.md @@ -421,21 +421,15 @@ switch from offline to online behavior. When a request supports offline behavior, the input states it explicitly or uses the same documented default as the CLI. -## Timeouts, cancellation, stdin, and bounded output +## Timeouts, stdin, and bounded output -The MCP client owns its request deadline. If it no longer wants the result, it -sends `notifications/cancelled`. The stdio adapter handles that notification -and response-size enforcement; it does not create or own the client deadline. -Operations expose focused cancellation support only when their behavior can -cooperate with it. +The stdio adapter owns response-size enforcement. - Subprocesses and network calls receive explicit server-side timeouts. -- A complex operation may accept a focused cancellation signal or - operation-owned timeout; there is no universal invocation context. +- A complex operation may define an operation-owned timeout; there is no + universal invocation context. - Transactional mutations roll back or report partial state according to their domain contract. -- After client cancellation, the server stops work and sends no tool result or - tool error for that request. - A server-enforced timeout while the request remains active returns a structured timeout error, never a successful empty result. - No operation reads stdin or inherits an interactive child stdin. @@ -483,8 +477,8 @@ Shared operation, CLI adapter, and parity coverage follows - Verify tool name, description, annotations, and exact input/output schemas. - Verify mapping to the shared operation request and outcome. - Verify structured warnings and tool errors. -- Verify trust failures, server-enforced timeout errors, client cancellation - without a response, and output-budget failures. +- Verify trust failures, server-enforced timeout errors, and output-budget + failures. - Verify project-directory mapping and operation dispatch without `os.chdir()`. - Verify standard tool annotations follow the conservative mapping and the inventory retains exact capability and network declarations. @@ -505,7 +499,6 @@ Shared operation, CLI adapter, and parity coverage follows - Keep an in-memory MCP registration and dispatch test. - Keep a real stdio initialize/list/call test with protocol-pure stdout. -- Verify `notifications/cancelled` stops cooperative work without a response. - Test malformed input, unavailable tools, internal failure sanitization, and output bounds as negative cases. @@ -668,7 +661,7 @@ For a new or migrated MCP operation: version are explicit. - [ ] Non-interactive behavior does not imply force, trust, or consent. - [ ] Project paths are normalized and passed explicitly without `os.chdir()`. -- [ ] Timeouts, cancellation, stdin, and output bounds are handled. +- [ ] Timeouts, stdin, and output bounds are handled. - [ ] Existing CLI human and JSON behavior remains compatible. - [ ] Operation, CLI adapter, MCP adapter, parity, and protocol tests cover positive and negative behavior. diff --git a/design/shared.md b/design/shared.md index b07b6e1220..9a704fb864 100644 --- a/design/shared.md +++ b/design/shared.md @@ -337,18 +337,13 @@ request lacks required consent. Machine-readable mode, non-interactive mode, transport authentication, or a host confirmation never implies `force`, trust, or destructive consent. -## Timeouts, cancellation, and bounded output +## Timeouts and bounded output Do not force every operation through a universal runtime object. -- The MCP client owns its request deadline and may send a cancellation - notification. The stdio adapter handles that signal and enforces response - size limits. -- A command that can cooperatively stop exposes a focused cancellation - parameter or operation dependency for that behavior. +- The stdio adapter enforces response-size limits. - Subprocess and network helpers receive explicit timeouts from the operation that invokes them. -- A server-enforced operation timeout is distinct from client cancellation. - Potentially large commands own pagination or limit fields in their request and result contracts. - Truncation is explicit and never returned as a successful complete result.