getgrav / getgrav/grav-plugin-api
MCP tool manifests: how should a tool say "this is the request body" when the field names are not known in advance?
- Dominant language
- PHP
- Stars
- 5
- Forks
- 8
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 2
Description
## Summary
The `mcp.yaml` manifest format works well when the plugin author knows every body field at authoring time (KahunaCart's case). It has no way to describe a route whose body fields are decided by the site, such as Flex Objects, where the fields come from each site's blueprints. Trying to write a Flex manifest against the 1.0.23 spec surfaced two gaps, one in the implementations and one in the spec itself. Since the format is new and only one manifest exists so far, this seems like the moment to settle it. I'd like your direction before anyone codes against a guess.
## The problem in plain terms
A manifest tool hands the model one flat JSON object to fill in. That object has to carry two different things: the **address** of the REST call (path placeholders and query parameters) and the **payload** (the JSON body). The MCP server pulls the address fields out, builds the URL, and sends whatever is left as the body.
For a route like `PATCH /flex-objects/{type}/{key}` the payload's keys are blueprint fields the manifest author cannot list. Two things go wrong:
1. **Undeclared fields were silently dropped.** The spec allows `additionalProperties: true` on the root `input`, and `McpToolCollector` preserves it (and assigns it to a properties-less object). But neither MCP server honored it at the root: grav-mcp hands the SDK a raw Zod shape, which strips unknown keys, and grav-plugin-mcp-server filtered to declared properties. The tool validated and listed fine; the fields never reached the route. Both servers honor it on nested objects. (grav-plugin-mcp-server 1.2.4 now passes them through when the root opts in; grav-mcp still strips.)
2. **Address and payload share one namespace.** Even with pass-through, a blueprint field named `type` or `key` is consumed as a path parameter and can never be set. No pass-through rule fixes this, because the manifest cannot say which property *is* the body.
The workaround available today is an envelope property (say `object`) holding the fields, with the plugin's controller unwrapping it when the body's only key is that envelope. That puts MCP knowledge into a REST controller and every plugin with this shape would repeat the heuristic.
## Options
**A. Add a per-tool `body` key** (my preference), parallel to the existing `query`:
```yaml
- name: update_object
method: PATCH
path: /flex-objects/{type}/{key}
body: object # this property's value IS the request body
input:
type: object
required: [type, key, object]
properties:
type: { type: string }
key: { type: string }
object: { type: object, additionalProperties: true, description: "Fields per the blueprint" }
```
Rules: `body` names a declared property of type `object`; it may not be a path placeholder or appear in `query`; when `body` is set, every other property must be a path placeholder or a query name, so nothing is left ambiguous. Controllers receive an ordinary REST body and never learn MCP exists. Path and field names can no longer collide.
**B. Rely on root `additionalProperties: true` alone.** Both servers pass undeclared arguments through; the spec documents that the root keyword means "open body". Cheapest, but the `type`/`key` collision stays, and the model sees address and payload mixed at one level.
**C. Leave the spec as is** and let plugins with dynamic bodies unwrap an envelope in their controllers. Works today, but each plugin re-implements the same guess.
Independently of A/B/C, the spec should state what root `additionalProperties: true` means: honored (pass-through) or rejected by the validator. Silent dropping was the worst outcome and should not be reachable.
## What each repo would change (under option A)
- **grav-plugin-api**: `body` in the README manifest table and `openapi.yaml`; validation in `McpToolCollector` per the rules above; echo `body` in `GET /mcp/tools` beside `query` and `path_params`; fixture tests; document the root `additionalProperties` rule. Lands first, since both servers read the field from this response.
- **grav-mcp**: `splitArguments` in `src/tools/plugin-tools.ts` uses `definition.body` when present; register tools so root `additionalProperties: true` is honored rather than stripped; `docs/plugin-tools-spec.md` and tests.
- **grav-plugin-mcp-server**: same handler change for `body` (root pass-through already shipped in 1.2.4); tests.
- **grav-plugin-flex-objects**: ship `mcp.yaml` with `body: object` on create and update; no controller change; declare the api release that understands `body` as its dependency so an older api rejects the manifest instead of serving a broken tool.
- **grav-plugin-kahunacart**: nothing. Its bodies have fixed fields and are unaffected either way.
Ordering matters for A: api first, then both servers, then Flex. A Flex manifest reaching a server that predates `body` would send `{"object": {...}}` literally.
## Questions
1. Is `body` the direction you want, or do you prefer B or C?
2. Root `additionalProperties: true`: honor or reject?
3. If A, any preference on the key name (`body`) and on whether leftover non-path, non-query properties are an error or ignored?
Happy to open the PRs against grav-plugin-api and grav-mcp once you've picked.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading the manifest table in README, openapi.yaml, and the McpToolCollector validation and fixture tests. Compare the proposed body behavior with the existing query and path_params fields, then review the linked server responsibilities and ordering. Done means the project has chosen an option, defined root additionalProperties behavior, and recorded the resulting API contract and tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- api
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100