Writing tools 

A tool is a PHP class implementing \Neoblack\Webmcp\Tool\ToolProviderInterface. Thanks to the #[AutoconfigureTag('webmcp.tool')] attribute on the interface, any autoconfigured service implementing it is picked up automatically – in this extension, a site package, or any third-party extension. No manual Services.yaml wiring is needed.

The interface 

interface ToolProviderInterface
{
    // Context-free, stable name. Used for the analytics whitelist before
    // the frontend is resolved. Must equal the Manifest name.
    public function name(): string;

    // Return the tool's Manifest, or null to omit it for this request
    // (e.g. a blog tool on a site without a blog). $processedData carries
    // the results of data processors that ran earlier in the same content
    // object, so you can build on them (e.g. a menu).
    public function manifest(ContentObjectRenderer $cObj, array $processedData): ?Manifest;
}
Copied!

The manifest 

new Manifest(
    name: 'search_articles',
    description: 'Search all articles …',
    inputSchema: [ /* JSON schema for the arguments */ ],
    primitive: Primitive::Search,
    data: [ /* primitive-specific payload, see below */ ],
    moduleUrl: null, // optional escape hatch, see below
);
Copied!

The runtime injects a client string property into every tool's input schema automatically (for the optional analytics hint), so you do not declare it yourself.

Primitives 

Every tool maps to exactly one primitive. The generic runtime interprets the data payload; you never write JavaScript.

mailto 

Build a pre-filled mailto: link and open it. No server storage.

primitive: Primitive::Mailto,
data: [
    'to' => base64_encode('me@example.org'),      // base64, kept out of source
    'subjectTemplate' => 'Request – {anliegen}',
    'bodyLines' => [                              // "Label: value" lines
        ['label' => 'Name', 'param' => 'name'],
        ['label' => 'Organisation', 'param' => 'org', 'optional' => true],
    ],
    'messageParam' => 'message',                  // free text block at the end
    'successTemplate' => 'A pre-filled e-mail to {to} has been opened.',
]
Copied!

static 

Return a curated list verbatim.

primitive: Primitive::StaticList,
data: [
    'items' => [['title' => 'Software', 'url' => 'https://…']],
    'resultKey' => 'services',
    'text' => ['heading' => 'Services:', 'line' => '{n}. {title} – {url}'],
]
Copied!

Template placeholders 

The text templates use {field} placeholders filled from the item (or, for headings, from {count} / {query}). {n} yields the 1-based index of the current line.

Escape hatch 

If no primitive fits, leave data minimal and set moduleUrl to the URL of an ES module exporting execute(args, ctx). The runtime imports it on first call and delegates to it.

Analytics 

Every tool call sends a same-origin beacon to the configured endpoint. The ToolRegistry::toolNames() list (all registered providers) is the whitelist the ingest middleware validates against – it follows your tools automatically.