Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

MCP tool manifests: how should a tool say "this is the request body" when the field names are not known in advance?

Open
#32 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Needs clarification
Activity status
Active
Tech stack
php
Domain
api

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.

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:

- 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.

Dominant language
PHP
Stars
5
Forks
12
Avg merge
9h 12m
Merged PRs (30d)
6

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from getgrav/grav-plugin-api

All issues in getgrav/grav-plugin-api

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.