Warning
Experimental. This extension is experimental and not yet ready for production use. It is built on top of WebMCP, which is itself an experimental, early-stage proposal. Both the underlying specification and this extension's API may change or break at any time without notice. Use at your own risk.
Writing tools
A tool is a PHP class implementing
\Neoblack\. Thanks to the
# 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. wiring is needed.
The interface
-
interface
ToolProviderInterface -
- Fully qualified name
-
\Neoblack\Webmcp\ Tool\ Tool Provider Interface
Implemented by every tool and tagged
webmcp.via the interface'stool #attribute.[Autoconfigure Tag] name()-
Context-free, stable tool name. Used for the analytics whitelist before the frontend is resolved, so it must equal the
Manifestname.- Returns
-
string– the stable tool name
manifest($cObj, $processedData)-
Build the tool's manifest for the current request.
- param ContentObjectRenderer $cObj
-
the current content object renderer
- param array $processedData
-
results of data processors that ran earlier in the same content object, so you can build on them (e.g. a menu)
- Returns
-
a
Manifest, ornullto omit the tool for this request (e.g. a blog tool on a site without a blog)
The manifest
-
class
Manifest -
- Fully qualified name
-
\Neoblack\Webmcp\ Tool\ Manifest
The serialisable description of a single tool. A provider returns one of these; the data processor collects them into the page's JSON block.
Construct it with named arguments:
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
// optional overrides, see the hint sections below:
// readOnly: …, title: …, untrustedContent: …, consequential: …, debugging: …
);
| Argument | Type | Purpose |
|---|---|---|
name | string | Tool name; must equal
Tool. |
description | string | Human-readable description shown to the agent. |
input | array | JSON schema for the tool arguments. |
primitive |
Primitive
| One of the four behaviour primitives. |
data | array | Primitive-specific payload (see below). |
module | ?string | Optional ES-module URL for the escape hatch. |
read | ?bool | Override the read-only hint. null (default) derives it from the
primitive; see below. |
title | ?string | Optional human-readable label for UI display. The machine-stable
name is used when omitted. |
untrusted | ?bool | Override the untrusted-content hint. null (default) derives it from
the primitive; see below. |
consequential | ?bool | Override the consequential hint. null (default) derives it from
the primitive; see below. |
debugging | bool | Mark the tool as a debugging aid. Default false; the annotation
is only emitted when set to true. |
Note
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.
Read-only hint
Each manifest carries a WebMCP annotations. flag telling the agent
whether the tool merely reads state or changes it. Agents use it to decide whether
a call may run without user confirmation.
The value is derived from the primitive: search and static are read-only,
navigate (changes the browser location) and mailto (opens a mail client)
are not. Pass read/false explicitly to override the default — for
example when an escape-hatch module built on the search primitive actually
mutates state.
Untrusted-content hint
The manifest also carries a WebMCP annotations. flag. It
tells the agent that the tool's output may contain untrusted, third-party data
that should be treated with caution — a prompt-injection defence.
It is derived from the primitive: only search is flagged, because its results
come from a JSON index that can hold user-generated content. static is curated,
and navigate / mailto only return messages the runtime built itself, so all
three default to false. Pass untrusted/false to override —
for instance when a static list is assembled from user-supplied data, or an
escape-hatch module returns third-party content.
Consequential hint
annotations. tells the agent that a call has a consequence
the user may want to review before it happens. It is derived from the primitive:
only mailto is flagged, because it hands a composed message to the visitor's
mail client — an action outside your site. navigate only moves within the
site (and can ask the user itself, see Confirming side effects), search
and static merely read. Pass consequential: true/false to override,
for example for an escape-hatch module that submits data.
Debugging annotation
debugging: true emits annotations., which the specification
defines for tools meant for development and testing. It is never set by default;
only set it for tools you do not want agents to use in normal operation.
Defaults per primitive
| Primitive | read |
untrusted |
consequential |
debugging |
|---|---|---|---|---|
navigate | false | false | false | omitted |
search | true | true | false | omitted |
mailto | false | false | true | omitted |
static | true | false | false | omitted |
Recommended limits
The specification requires tool names of 1–128 characters from
A-. Beyond that, Chrome's
Secure tools
guidance recommends tighter budgets. They are recommendations, not rules:
| What | Recommended maximum |
|---|---|
| Tool name, parameter name | 30 characters |
| Tool description | 500 characters |
| Parameter description | 150 characters |
| Text output of one call | 1,500 characters |
The
\Neoblack\ checks every
manifest with
\Neoblack\ and writes a
warning to the TYPO3 log for each name or description over these budgets and
for names outside the specification's pattern. The tool is emitted regardless.
The output budget is enforced at runtime via outputLimit.
Note
Tools are only registered in the top-level document, never inside iframes (see Configuration).
Primitives
Every tool maps to exactly one primitive. The generic runtime interprets the
data payload; you never write JavaScript.
Fetch a same-origin JSON index, filter it by the query terms, return structured hits.
primitive: Primitive::Search,
data: [
'indexUrl' => 'https://…/index.json',
'queryParam' => 'query',
'limitParam' => 'limit',
'limitDefault' => 10,
'queryRequired' => true, // false: list all when empty
'searchFields' => ['title', 'teaser'], // fields searched
'resultKey' => 'results',
'resultFields' => ['title' => 'title', 'cat' => 'categoryLabel'],
'deepLinkTemplate' => 'https://…/blog#q={query}', // optional
'text' => [ // optional output templates
'heading' => '{count} hits for „{query}“:',
'headingAll' => '{count} entries:',
'line' => '{n}. {title} – {url}',
'emptyQuery' => 'No hits for „{query}“.',
'emptyAll' => 'No entries.',
],
]
Tip
result may be a list (pick fields 1:1) or a map
(output => source rename); omit it to pass items through
unchanged.
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
'confirm' => 'Send this e-mail to {to}?', // optional; see below
'successTemplate' => 'A pre-filled e-mail to {to} has been opened.',
]
Return a curated list verbatim.
primitive: Primitive::StaticList,
data: [
'items' => [['title' => 'Software', 'url' => 'https://…']],
'resultKey' => 'services',
'text' => ['heading' => 'Services:', 'line' => '{n}. {title} – {url}'],
]
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.
Confirming side effects
The navigate and mailto primitives change state — they move the browser or
open a mail client. Set a confirm message on their data to require a
human-in-the-loop confirmation before the side effect runs. The runtime uses
request where the browser offers it on the execute
callback's second argument and otherwise a plain confirm.
Note
The current specification draft (2026-10-02) does not define
request; Chrome lists it as planned. In today's
implementations the confirm fallback is therefore the usual path. If the user declines, the tool returns an is result
(navigate uses the cancelled message when set) and the side effect never
happens.
Omit confirm to keep the previous behaviour — the tool runs without asking. The
confirm string is filled with the same {placeholders} as the other
messages (navigate: the chosen option; mailto: {to} and {subject}).
Escape hatch
If no primitive fits, leave primitive at any value, keep data minimal and
set module to the URL of an ES module exporting an execute function.
The runtime imports the module lazily — on the tool's first call — and delegates
to it. A module is only used when no built-in primitive matches, so a
custom module always wins the fallback, never overrides a primitive.
The contract
// served same-origin, or CORS-enabled for dynamic import
export function execute(args, ctx) {
// args: the tool arguments the agent passed, already normalised
// (the runtime unwraps an { arguments: {…} } envelope for you).
// Includes the optional `client` analytics hint if the agent set it.
//
// ctx: { tool, config, client }
// ctx.tool – this tool's manifest object
// { name, description, inputSchema, primitive, data, moduleUrl }
// ctx.config – the whole page manifest { endpoint, tools: [...] }
// ctx.client – the second argument the browser passed to execute():
// in the current draft an object with an AbortSignal
// ({ signal }); older implementations passed a client
// with requestUserInteraction(). May be undefined.
// Return an MCP tool result. `content` is required; `structuredContent`
// is optional machine-readable output. You may return a Promise.
// Text content is capped at the configured outputLimit.
return {
content: [{ type: 'text', text: 'Done.' }],
structuredContent: { ok: true },
};
}
Tip
Analytics is already handled. The runtime fires the usage beacon before
importing your module, so you do not call the endpoint yourself. Keep data
for your own config and read it from
ctx..
Warning
Errors surface to the agent. A thrown error or rejected Promise
propagates as the tool call's failure — for a recoverable "no match /
unavailable" case return a normal result with is instead of
throwing, so the agent learns the call failed but the page stays healthy:
return { content: [{ type: 'text', text: 'No match.' }], isError: true };
Loading is lazy and best-effort: a failed import fails only that one call,
nothing else on the page is affected. The built-in primitives already flag
their failure paths (e.g. navigate with an unknown option) this way.
Analytics
Every tool call sends a same-origin beacon to the configured endpoint. The
\Neoblack\ list (all registered
providers) is the whitelist the ingest middleware validates against – it follows
your tools automatically.
See also
- Quickstart – a full end-to-end example using the
staticprimitive. - Architecture – how providers, the registry, the processor and the runtime fit together.
- Analytics – what the beacon records and how the ingest endpoint is protected.