Tools

← Tools

Ashterix

docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api

en/docs/api/mcp-bundle/tools.md new file +127 āˆ’0

1 + # Tools
2 +
3 + Every RPC method of the endpoint's API group and version is an MCP tool. `tools/list` returns them, `tools/call` executes them through the JsonRpcBundle core. There is no separate allowlist or MCP registration: only membership in an [API group](/docs/api/json-rpc-bundle/functionality/groups) decides publication.
4 +
5 + ## Name
6 +
7 + The tool name is the RPC method name (`Catalog.search`). MCP allows names matching `^[a-zA-Z0-9_.-]{1,128}$`.
8 +
9 + :::danger
10 + A method of the group whose name does not match the pattern (for example, `#[RPC\Info('Bad', concat: ':')]` gives `Bad:ping`) makes **every request to that group's MCP endpoint** fail with HTTP 500; in debug mode the error explains which method and why. Other groups and the rest of the application are not affected, and methods outside MCP groups are never checked. The bundle deliberately does not skip such a method silently.
11 + :::
12 +
13 + ## Title and description
14 +
15 + They come from the [core translations](/docs/api/json-rpc-bundle/documentation) in the language negotiated by `Accept-Language` (the core `RpcLocaleNegotiator`). The response carries `Content-Language` and `Vary: Accept-Language`.
16 +
17 + ```
18 + translations/rpc.uk.yaml # base translation, all groups
19 + translations/rpc/mcp.uk.yaml # only for the mcp group, e.g. texts for an AI agent
20 + ```
21 +
22 + ```yaml
23 + # translations/rpc/mcp.en.yaml
24 + Catalog.search:
25 + description: Search products by name. Returns up to limit products, the most relevant first.
26 + ```
27 +
28 + Each field has its own fallback chain: group + client language → group + default language → base + client language → base + default language → `#[RPC\Info]` / PHPDoc. If there is no title anywhere, the tool name becomes the title. The translation key is the method name, so a translation applies to all versions of the method.
29 +
30 + :::warning
31 + Do not name a group the same as a Symfony Translator domain (`messages`, `validators`, …).
32 + :::
33 +
34 + ## inputSchema
35 +
36 + Built from the method metadata in the core, the same source as the OpenRPC document:
37 +
38 + * parameters → `properties`; parameters without a default value → `required`; default values → `default`;
39 + * `@param` descriptions → `description`;
40 + * DTOs and enums → `$ref` to the tool's own `$defs`: OpenRPC `#/components/schemas/...` references are rewritten, so every tool is self-contained, including recursive DTOs;
41 + * `additionalProperties: false`.
42 +
43 + ```php
44 + /**
45 + * Ping.
46 + * @param string $who Who is pinging
47 + */
48 + public function ping(string $who = 'x'): string
49 + ```
50 +
51 + ```json
52 + {
53 + "type": "object",
54 + "properties": {
55 + "who": {"type": "string", "description": "Who is pinging", "default": "x"}
56 + },
57 + "additionalProperties": false
58 + }
59 + ```
60 +
61 + The SDK validates `arguments` against the schema before execution: an unknown argument, a missing required one or a wrong type gives a JSON-RPC error `-32602` in the MCP response (not a tool result with `isError`), and the method is not called. This also covers the core special parameter `$rpc`, which is why asynchronous calls and webhooks are not possible through MCP.
62 +
63 + ## outputSchema
64 +
65 + The method's return type as the core describes it for OpenRPC, wrapped the same way the result is returned:
66 +
67 + ```json
68 + {"type": "object", "properties": {"result": {"type": "string"}}, "required": ["result"]}
69 + ```
70 +
71 + * Every `"type": "array"` becomes `["array", "object"]`: a PHP array serializes to a JSON list or a JSON object depending on its keys (`['a' => 1]` → `{"a":1}`), and clients that validate `structuredContent` against the schema would reject the object.
72 + * A DTO is a `$ref` to the tool's own `$defs`.
73 + * There is no `outputSchema` when the method has no return type or returns `object`: the core describes such a result as "any object", and that schema would reject a scalar.
74 +
75 + ## Annotations: `#[McpTool]`
76 +
77 + Optional MCP `ToolAnnotations` hints on a method. The attribute does not affect publication.
78 +
79 + ```php
80 + use Ufo\McpBundle\Attribute\McpTool;
81 +
82 + #[McpTool(readOnlyHint: true, idempotentHint: true)]
83 + public function search(string $query, int $limit = 10): array
84 + ```
85 +
86 + | Parameter | Value `true` |
87 + | --- | --- |
88 + | `readOnlyHint` | The tool does not modify its environment |
89 + | `destructiveHint` | It may perform destructive updates (`false`: only additive ones) |
90 + | `idempotentHint` | Calling it again with the same arguments has no further effect |
91 + | `openWorldHint` | It interacts with an "open world" of external entities (for example, the web) |
92 +
93 + All parameters are `?bool`: `null` means "not specified" and is left out of the output.
94 +
95 + ## tools/call
96 +
97 + A call becomes an `RpcRequest` (the `RpcFromMcp` carrier: method = tool name, params = arguments, version from the route) and runs through the core `RpcExecutor`, the same pipeline as `/rpc`: [parameter and DTO conversion](/docs/api/json-rpc-bundle/add_rpc_service/params), [`#[RPC\Assertions]`](/docs/api/json-rpc-bundle/add_rpc_service/assertions), [`#[RPC\Cache]`](/docs/api/json-rpc-bundle/add_rpc_service/cache), [`#[RPC\Lock]`](/docs/api/json-rpc-bundle/add_rpc_service/lock), [events](/docs/api/json-rpc-bundle/functionality/events). The response is normalized by the core serializer, exactly as `/rpc` returns it.
98 +
99 + **Success:**
100 +
101 + ```json
102 + {
103 + "content": [{"type": "text", "text": "{\"id\":7,\"name\":\"n7\"}"}],
104 + "structuredContent": {"result": {"id": 7, "name": "n7"}},
105 + "isError": false
106 + }
107 + ```
108 +
109 + `structuredContent` is for clients that read structured output (validated against `outputSchema`); the same JSON in `TextContent` is for the rest.
110 +
111 + **An error** (an exception in the method, a validation error or an exception outside the method, for example in an `rpc.request` listener) is a core RPC error with `isError: true` and no `structuredContent`:
112 +
113 + ```json
114 + {
115 + "content": [{"type": "text", "text": "{\"code\":-32603,\"message\":\"...\",\"data\":{\"exception\":\"Ufo\\\\RpcError\\\\RpcInternalException\"}}"}],
116 + "isError": true
117 + }
118 + ```
119 +
120 + `data` follows the [core rules](/docs/api/json-rpc-bundle/error_handling) for the environment: full details (`file`, `line`, `trace`) in `dev` / `test`, elsewhere only the exception class and `extra` (validation details). Every error goes through the `rpc.error` event and the core logging.
121 +
122 + **Events and cache:**
123 +
124 + * [`rpc.post_response`](/docs/api/json-rpc-bundle/functionality/events/post_response) fires on `kernel.terminate`, after the response is sent, once per executed call, also for several `tools/call` in one JSON-RPC batch. It does not fire for an error response or a cached one (a core rule).
125 + * `#[RPC\Cache]` works the same as on `/rpc`: a cached call does not execute the method.
126 +
127 + The `id` of the MCP request's JSON-RPC envelope belongs to the client; the internal RPC request `id` is generated for each call and never exposed.