| name | system-convert-remote-event-to-mcp-tool |
| description | Use when exposing an existing `origin: ["remote"]` listener as an MCP tool without rewriting the handler. Covers the one-line manifest pattern that re-uses the same class+method, plus the minimal schema work needed to satisfy `McpToolsListHandler::collectTools()`. Complements system-create-mcp-tool (which covers the greenfield case). |
Convert a remote Event into an MCP Tool
Use When
- A
remote web event already returns the exact payload you want to expose.
- The handler does not need a new method, different arguments, or a different response shape.
- You want the tool in
tools/list and dispatchable via tools/call with zero handler code changes.
If the response shape, auth, or origin needs to change, use system-create-mcp-tool instead.
The Pattern (1 manifest entry + 2 schema files)
1. Manifest
Add a second listen entry that points at the same class+method as the remote one, with origin: ["mcp"] and the tool name as the event:
{
"event": "example:pricing:list",
"class": "Example\\App\\Api\\AlertApi",
"method": "onCountries",
"origin": ["remote"]
},
{
"event": "getPricingList",
"description": "<human-readable description for the tools/list catalogue>",
"class": "Example\\App\\Api\\AlertApi",
"method": "onCountries",
"origin": ["mcp"],
"schema": {
"request": "module://<module>/schemas/<name>Request.json",
"response": "module://<module>/schemas/<name>Response.json"
}
}
The gateway uses tools/call params.name verbatim as the event type and runs the listener with params.arguments as the event request. The handler runs unchanged.
2. Schemas (place in <module>/schemas/ — NOT schema/mcp/)
schemas/<name>Request.json — mirror the event's request keys exactly. Include every key the handler reads from $event->request; mark optional ones by leaving them out of required.
schemas/<name>Response.json — mirror the $event->response shape the handler actually sets. Both data and its sub-objects go under properties. Use additionalProperties: false on every object node so the gateway rejects drift.
3. Bump version and reload
Bump the module version and run bin/zolinga (no parameters). The manifest cache regenerates; the new tool appears in the next tools/list response.
Schema Gotchas
Empty properties becomes [] on the wire
McpToolsListHandler::loadSchema() does json_decode($contents, true). An empty JSON {} becomes an empty PHP array, which json_encode re-serialises as []. Strict MCP clients then reject the tool with "Incorrect type. Expected 'object'".
Fix — always give properties at least one entry, even if no-op:
{
"type": "object",
"properties": {
"_": {
"type": "null",
"description": "Reserved placeholder. The tool takes no arguments; send {}."
}
},
"additionalProperties": false
}
Clients that send arguments: {} validate fine (the _ property is optional and absent), and the gateway emits a JSON object on the wire.
Response must include every field the handler emits
If the handler returns search on each country, the response schema must list it (or set additionalProperties: true). Otherwise strict clients reject structuredContent as non-conforming — even though tools/call itself succeeded.
Schema dir location is part of the manifest, not the convention
module://<module>/schemas/... and module://<module>/schema/mcp/... are both valid Zolinga URIs. The dir is whatever you point at in zolinga.json; pick one and keep all your tools' schemas in the same place.
When the Pattern Is Not Enough
Add a real handler class (back to system-create-mcp-tool) when you need any of:
- Different arguments than the
remote event accepts.
- Different response shape (e.g. strip a field, wrap in
{ data: ... }).
- Different auth (the
remote event may have "right": "member of users"; the MCP version should have preferably "right": "oauth2:scope mcp:tools" to distinguish that AI can run only events specifically authorized for it using allowed OAuth2 scope). If the API does not require login omit right entirely.
- Per-tool caching, validation, or side-effects before/after the
remote call.
Smoke Test
curl -X POST http://localhost:8080/mcp -D /dev/stderr -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'
curl -X POST http://localhost:8080/mcp -D /dev/stderr -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"getPricingList","arguments":{}}}' | jq
A successful invocation returns:
{
"jsonrpc":"2.0","id":2,
"result":{
"content":[{"type":"text","text":"..."}],
"isError":false,
"structuredContent":{...}
}
}
Checklist