Drop CustomCallToolHandler in favour of mcp/sdk RunTimeHandlerInterface
### Problem/Motivation Today `mcp_server` works around `mcp/sdk` by registering a bespoke request handler — `Drupal\mcp_server\Handler\CustomCallToolHandler` — that intercepts every `CallToolRequest` *before* the SDK dispatches it, then dispatches an `McpAuthorizeCallEvent` and ultimately invokes our `ToolPluginInterface::executeTool()` directly. This bypass exists because the SDK's `Mcp\Capability\Registry\ReferenceHandler` only knew how to invoke handlers shaped as a `\Closure`, a `[$objectOrClass, $method]` pair, or a class string — there was no way to register an object that *itself* knows how to execute and how to filter the inbound argument bag. The cost of the workaround: - We register every tool with a `static fn() => NULL` placeholder handler purely so the SDK accepts the registration. The real call path lives outside the SDK in `CustomCallToolHandler::handle()`. - Authorization, gateway construction, and argument filtering are duplicated in our handler instead of riding on the SDK's normal dispatch. - It diverges from the SDK's design: anyone reading the code has to learn that `addRequestHandler()` plus a no-op tool registration is what actually wires tools up. - Two adjacent classes (`McpAuthorizeCallEvent`, `McpOperationDescriptor`) only exist to serve the bespoke handler, expanding the API surface for no upstream benefit. We have prototyped a small, additive change to `mcp/sdk` that introduces a new handler shape: `Mcp\Server\Handler\RunTimeHandlerInterface`. When `ReferenceHandler::handle()` finds an instance of this interface as the registered handler, it calls `$handler->execute($handler->filterArguments($arguments), new ClientGateway($session))` and we're done — no custom request handler, no placeholder closures, no parallel dispatch path. ### Proposed resolution 1. Land the SDK change upstream (or carry it as a Composer patch in the meantime). The patch adds: - `Mcp\Server\Handler\RunTimeHandlerInterface` with `filterArguments(array): array` and `execute(array, ClientGateway): mixed`. - A new branch in `Mcp\Capability\Registry\ReferenceHandler::handle()` that detects the interface and dispatches to it. - The `Handler` phpstan type alias on `ElementReference` extended to include the new interface. 2. Update `Drupal\mcp_server\Plugin\ToolPluginInterface` to extend `RunTimeHandlerInterface` and remove the legacy `executeTool(?ToolRegistration, string, array, ?ClientGateway)` method. 3. Provide a default `filterArguments()` implementation on `ToolPluginBase` that strips keys beginning with `_` (the convention we already use for internal/system arguments). 4. Refactor every shipped tool plugin (`EchoTool`, `ContentLookupTool`, `ToolApi`) to implement `execute(array $arguments, ClientGateway $gateway): mixed` instead of `executeTool()`. 5. In `McpServerFactory::registerTools()`, instantiate each `ToolPluginInterface` and pass the *plugin instance itself* as the handler to `Builder::addTool()` — replacing the placeholder closure registration. 6. Have `ToolPluginManager::createInstance()` resolve and inject the matching `ToolRegistration` into the plugin's configuration so plugins that need it (e.g. `ToolApi`, which keys behaviour off `McpToolConfig`) can reach it via `$this->getConfiguration()['registration']`. 7. Delete `Drupal\mcp_server\Handler\CustomCallToolHandler`, `McpAuthorizeCallEvent`, and `McpOperationDescriptor`. Remove the `addRequestHandler()` call from `McpServerFactory::buildServer()`. 8. Move the authorization concern (previously raised via `McpAuthorizeCallEvent`) into the existing per-plugin access checks (`ToolPluginInterface::access()` / `accessOperation()`), or expose an equivalent SDK-friendly hook if a request-time event is still required. 9. Update `references/tools/index.md` and `references/auth/index.md` to describe the new flow and drop references to the deleted classes/events. ### Remaining tasks - Open the upstream PR against `mcp/sdk` and link it here. - Decide whether `mcp_server_oauth` needs a replacement for `McpAuthorizeCallEvent` or whether the per-plugin `access()` path is sufficient. - Update tests that subscribed to `McpAuthorizeCallEvent` (if any) to use the new access path. - Verify HTTP `/_mcp` and Drush STDIO transports still execute tools correctly end-to-end. ### User interface changes None. ### API changes - `ToolPluginInterface::executeTool()` is removed. Implementers must provide `execute(array $arguments, ClientGateway $gateway): mixed` instead. - `McpAuthorizeCallEvent` and `McpOperationDescriptor` are removed. Subscribers must move authorization logic into the plugin's `access()` / `accessOperation()` methods. - These are breaking changes; per `AGENTS.md` the 2.x branch carries no backwards-compatibility shims and they ship as-is. ### Data model changes None.
issue