Wymcp.Tool.Schema (Wymcp v0.6.1)

View Source

Builds the JSON Schema inputSchema for Wymcp tools.

One shape: the action field lists the declared action names as an enum whose description carries the action summaries, one per line, and data is a bare object. build/1 and action_summaries/1 render those summaries from one private function, so the enum description and the help index cannot drift. The newline separator stays unambiguous because Wymcp.Tool.Actions.validate!/1 — run at every moment a tool's schemas are validated — rejects a newline in an action's name or its description, the two halves a summary joins. Wymcp.Tool's generated input_schema/0 is the only caller of build/1.

Both public functions take the tool module, not an actions map, and obtain the schemas themselves through Wymcp.Tool.Actions.fetch_schemas!/1 — so there is no way to hand this module a map that no validator has seen, and the direct schema.description read below needs no check of its own. Each obtains once and hands the map to a private function: obtaining twice on one path could publish an action enum that disagrees with its own summaries, for a tool whose Wymcp.Tool.actions/0 varies between calls.

The key set is closed and said so out loud: the root carries additionalProperties: false, which is the server stating its contract in tools/list so a schema-aware client can catch a misspelled key before it sends. Enforcement is not this schema's job — argument validation checks structure only, and a dispatch gate answers a stray key in the tool dialect, naming the key and pointing at help (see "Dispatch errors and self-correction" in Wymcp.Tool). The declaration is the root's alone: data stays a bare object, so a tool that nests free-form structure under it is unaffected.

The action property carries one further key: the header annotation "x-mcp-header" => "Action", which asks a conforming modern client to mirror the chosen action into an Mcp-Param-Action request header. It is the only property a generated schema can annotate — the spec reaches a property through "properties" keys alone, and data declares none — and it costs a tool author nothing, since the framework writes it. What a gateway gets for it is the tool and the action of every call without parsing a body. header_annotations/1 reads such annotations back out of any input schema, generated or hand-written, and validate_header_annotations!/2 is what a hand-written one is held to.

Per-action constraints are deliberately not encoded here: :required and :required_one_of are enforced at dispatch by Wymcp.Tool, and the full per-action schemas are surfaced on demand by Wymcp.Help. Property values (types, formats) are not validated by the framework at all — a tool that needs value guarantees checks them in run_action/3. This keeps the tools/list payload compact; agents act from the action summaries and pay for a tool's full schemas only when they ask.

Summary

Functions

An action summary is one action's name joined to its description as "<action>: <description>". Returns one summary per action of module, sorted by action name.

The header annotations schema declares, sorted by property path.

Validates every header annotation in schema, raising ArgumentError naming module and the rule it breaks — and the property, wherever there is one to name.

Types

json_schema()

@type json_schema() :: %{required(String.t()) => term()}

Functions

action_summaries(module)

An action summary is one action's name joined to its description as "<action>: <description>". Returns one summary per action of module, sorted by action name.

What the help tool's server index renders from. build/1 renders the tools/list action description from the same private function rather than from this one, so a change made here alone reaches the index only.

Example

Given a tool whose Wymcp.Tool.actions/0 returns

%{
  get: %{description: "Get a widget", properties: %{}},
  create: %{description: "Create a widget", properties: %{}}
}

the summaries are

["create: Create a widget", "get: Get a widget"]

build(module)

header_annotations(schema)

The header annotations schema declares, sorted by property path.

Each entry is a map of :header — the annotation's value, the display half of the Mcp-Param-* name a conforming client sends — :path, the property's keys from the schema root, which is also the path into a call's arguments, and :type, the property's declared JSON Schema type.

Only properties reachable from the root through "properties" keys are returned, because those are the only ones the spec lets a client honour. An annotation anywhere else is not silently ignored: it is what validate_header_annotations!/2 refuses, by counting both sets and comparing them.

validate_header_annotations!(module, schema)

Validates every header annotation in schema, raising ArgumentError naming module and the rule it breaks — and the property, wherever there is one to name.

The rules are the spec's, and a conforming client enforces the same set by excluding a tool whose schema breaks any of them from its own tool list — silently, as far as the server is concerned. Refusing the schema at the registration moment is what turns that disappearance into a message naming the tool.

Only a hand-written Wymcp.Tool.input_schema/0 can break a rule: the generated schema's one annotation is the framework's own. That callback is also the one caller that can return something other than a JSON Schema object — a struct included — and a return like that is refused here too, naming the tool and this rule and no property, since there is none: both registration moments hand this function whatever the callback returned, and nothing upstream checks its shape.