Proposal: make Tool's declarations introspectable over the CLI, and enrich input schema
## Goal Tool stays consumer-agnostic. Give consumers — the Drush CLI, `mcp_server`, a future planner — enough *introspectable* metadata to understand and present a tool they didn't author, **before** invoking it. Most of what's needed already exists in the Tool API model; the work is mostly **surfacing** it over the CLI, plus **one genuinely additive** piece (richer input schema). Two deliverables (A additive, B surfacing); each can ship as its own MR. This issue is the umbrella proposal for a shared discussion. ## Background This comes out of building [`canvas_tools`](https://www.drupal.org/project/canvas_tools) (a local sandbox: ~15 Tool API ops wrapping the `@internal` Drupal Canvas API, over both `drush tool:run` and MCP via `mcp_server_tool_bridge`). A consumer can read *what tools exist* (`tool:list` / `tool:info`) but not enough about a tool's inputs or nature to drive it safely. A pass over the model confirmed the operational metadata is already declared — `operation`, `destructive`, typed outputs, `access()`, `checkRequirements()`. So this is mostly "surface what's declared," not "add a model," except for input schema. Runtime operand discovery — the actual valid values at call time (which components exist, which UUIDs are on a page right now) — is a separate, larger discover/plan/execute concern and explicitly out of scope here. ## Deliverable A — enrich input schema in the definition + CLI (additive) Today an input flattens to its data type. `tool:info` shows `variant: string`, not that it accepts only `info|success|warning`; shows `props: map`, not its keys. - Surface the complete typed-data schema per input: allowed values (`AllowedValues` / enum), the property definitions of a `MapInputDefinition`, the item definition of a list. - Emit it as a **Drupal-native structured array** from `tool:info` and `--format=json` — no JSON Schema dependency in Tool, friendlier over Drush. - Consumers transform as needed: `mcp_server` (which already builds `inputSchema` for bridged tools from these same `InputDefinition`s) maps the native structure → MCP JSON Schema. Enriching the source improves the CLI and the MCP surface in one change. - Scope: statically declared constraints only. ## Deliverable B — surface the operational metadata Tool already declares Tool already declares all of the following; `tool:info` only prints some of it. Make it all introspectable so a consumer can plan without invoking. | Capability | In Tool today | Surfacing gap | |---|---|---| | Operation kind | `ToolOperation` enum: `Explain` / `Read` / `Transform` / `Trigger` / `Write`, with `isModifying()` and `isIdempotent()` | Printed in `tool:info` markdown; confirm `--format=json` carries it | | Destructive | `destructive: bool` on `#[Tool]` | Printed in `tool:info`; confirm json | | Typed outputs | `output_definitions` (`ContextDefinition`), `getOutputValues()` | Declared and printed; #3582942 fixed the CLI swallowing them on failure. Follow-up: enrich output schema like inputs in A | | Access check | `access()` + per-tool `checkAccess($values, $account)` returning `AccessResult` | Enforced at runtime, but the **required permissions aren't listed** in `tool:info` | | Static requirements | `checkRequirements()` — throws preconditions (config/API key) before invocation | Not surfaced in `tool:info`, and `tool:list` doesn't flag/filter tools whose requirements fail | The two real CLI gaps are **access** (list required permissions) and **requirements** (surface unmet preconditions, and let `tool:list` flag or filter tools an agent can't run, so it never sees an unusable tool). ## Side-effect-free comes from `operation`, not a new property An earlier draft of this proposal floated a new `sideEffectFree` flag — dropped. `operation` already encodes it: `isModifying()` is FALSE for `Explain` / `Read` / `Transform`, TRUE for `Write` / `Trigger`. A consumer derives side-effect-free from `operation`; a parallel property would just duplicate it. The enum also maps almost directly onto the MCP surfacing axis. A consumer (e.g. `mcp_server`) decides the surface from `operation` plus a locator-vs-query read of the inputs: | operation / inputs | MCP surface | |---|---| | `Explain` / `Read`, no inputs | Resource (fixed URI) | | `Explain` / `Read`, inputs all *locate* one resource | Resource Template | | `Transform` (read-only but inputs query/derive), or `Write` / `Trigger` | Tool | [Resource Templates](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates) are RFC 6570 URI templates and take multiple variables, so a compound key is still addressable: `node/{id}`, `node/{id}/revision/{vid}`, `node/{id}/translation/{langcode}`. Notes: - A non-modifying op with *scalar* inputs can still be a Tool — a search taking `keyword` + `limit` doesn't mutate and its inputs are scalar, but they parameterize a result set rather than locate a resource (MCP convention: search is a Tool, not a resource). Scalar ≠ locator. `Transform` covers this. - Compound keys (revision = id+vid, translation = id+langcode) need no collapsing — multi-variable templates cover them. - `locator`-vs-`query` only splits `Read` into Resource vs Resource Template; the modifying/non-modifying half is fully covered by `operation`. Tool carries no URI-template knowledge; the Resource / Template / Tool decision lives in the consumer. ## Sequencing A and B are independent. A is additive and immediately useful to the CLI alone. B is mostly surfacing existing data — small, and its planning payoff lands once a consumer reads it. ## Out of scope (separate efforts) - `mcp_server` surfacing logic + Resource / Resource Template registration — that lives in the `mcp_server` queue and needs its maintainers' sign-off. - Runtime operand discovery (valid input values at call time). ## Open questions - A: shape of the native schema array — mirror typed-data's own definition structure, or a flatter purpose-built one? - B: should `operation` be an *enforced* contract? Nothing currently stops a `Read` tool from mutating, which is what a consumer trusts when it caches or exposes the op as read-only. - B: should an input be flaggable as a *locator* (identity) input, so the consumer's Resource-Template-vs-Tool decision is deterministic rather than heuristic? Cheap hint, no URI knowledge in Tool — but it is consumer-facing metadata, so it may belong on the `mcp_server` side instead. ## Related - #3582942 — `tool:run` was masking failure messages by reading outputs on a failed tool (came out of the same testing).
issue