Problem/Motivation
The module's MCP transport layer is built on swisnl/mcp-client, pinned to ^0.5 with minimum-stability: dev. That library is a third-party, pre-1.0 implementation of the Model Context Protocol, and the module already has to work around it in several places:
Drupal\mcp_client\Transporter\CustomHeadersHttpTransporterextendsSwis\McpClient\Transporters\StreamableHttpTransporterpurely to inject custom HTTP headers, because the library offers no supported way to do it.- The same class overrides
initializeConnection()to strip the library'sregister_shutdown_function()calls, which stop the ReactPHP event loop before Drupal's request lifecycle is finished and produce "Connection aborted early" errors. The override then reaches into the parent with\ReflectionMethodto call the privateupdateSessionIdFromResponse(). MCPClient::getStderrErrors()uses\ReflectionClassto read the client's protectedtransporterproperty just to surface STDIO stderr output on a failed connection.MCPClient::listTools()andMCPClient::executeTool()hand-translate the library's result objects back into plain arrays, includingmethod_exists()probing on content items because the content types are not modelled in a way the module can rely on.
Three of these are reflection against private or protected internals. Every one of them is a candidate to break on any 0.x release of the upstream library, and none of them are covered by an upstream compatibility promise.
Meanwhile, the Model Context Protocol project now ships an official PHP SDK - modelcontextprotocol/php-sdk - developed together with the Symfony team, with a documented Client SDK. It tracks the MCP specification directly, uses Symfony HttpClient and Process rather than ReactPHP, and exposes transports, headers and STDIO configuration as first-class supported API rather than as things to be reached around.
Proposed Resolution
Replace swisnl/mcp-client with the official modelcontextprotocol/php-sdk as the underlying client, keeping MCPClient's public surface (listTools(), executeTool(), disconnect()) unchanged so that McpClientFactory, McpToolBase and McpToolDeriver need no behavioural changes.
- Swap the Composer requirement from
swisnl/mcp-client: ^0.5to the official SDK, and re-evaluate whetherminimum-stability: devis still needed once the dependency changes. - Rebuild
MCPClient::createHttpClient()on the SDK's HTTP transport, passing custom headers through the SDK's supported configuration instead of subclassing a transporter. DeleteCustomHeadersHttpTransporterentirely, along with itsinitializeConnection()override and the\ReflectionMethodcall inside it. - Rebuild
MCPClient::createStdioClient()on the SDK's STDIO transport. The SDK builds on Symfony Process, socommand,envandcwdmap across directly, and the$this->processreference held only to defeat garbage collection can likely be dropped. - Replace
getStderrErrors()- the SDK is expected to surface process stderr through its own exception or transport API, so the\ReflectionClassaccess to the protectedtransporterproperty can be removed. If no such API exists, this is the one gap worth raising upstream rather than working around locally. - Map the SDK's tool and content result objects to the arrays that
listTools()andexecuteTool()already return, replacing themethod_exists()probing with typed handling of the SDK's content types. - Confirm the ReactPHP event-loop lifecycle problem disappears. The Symfony-based SDK is synchronous, so there should be no loop to stop and no shutdown handler to unregister - this is the main reason the current transporter override exists.
Existing tests in tests/src/Unit/MCPClientTest.php and the kernel tests under tests/src/Kernel/ should pass unchanged against the new implementation; where they mock Swis\McpClient\Client directly they need retargeting at the SDK's client class. A test asserting that a configured HTTP header actually reaches the outgoing request, and one asserting that a failed STDIO connection reports the child process's stderr, would lock in the two behaviours that the reflection hacks currently provide.
Remaining Tasks
- Verify the official SDK's client covers both Streamable HTTP and STDIO transports at the maturity this module needs, and note its stability level.
- Check whether the SDK exposes STDIO stderr; if not, open an upstream issue before removing
getStderrErrors(). - Perform the swap and delete
CustomHeadersHttpTransporter. - Retarget the unit tests and add coverage for custom headers and STDIO connection failure.
- Manually test against a real HTTP MCP server and a real STDIO MCP server.
- Update
README.mdand thedocs/pages that name the underlying library.
User Interface Changes
None. Server configuration and the tool plugins behave exactly as before.
API Changes
Drupal\mcp_client\Transporter\CustomHeadersHttpTransporter is removed. It is an implementation detail of MCPClient and is not referenced anywhere else in the module, but it is a public class, so this is technically a BC break for any site that extended it. MCPClient, McpClientFactory and the tool plugins keep their signatures.
AI Assistance
Yes, AI was used to explore the issue.
Issue fork mcp_client-3609590
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
guignonvQuestion: meanwhile a migration decision is made, should people continue contributing when issues are related to the `swisnl/mcp-client`?
I'm currently facing an issue with Swis\McpClient\Client->await() (React\Async\await() returning null) which is blocking. Should I got better "wait" for a migration that may change the issue? ...for how long? I just need to evaluate my options and schedule things. :)
Comment #3
arianraeesi commentedComment #4
marcus_johansson commented@guignonv - AI Initiative has sprints and this has been picked, so it is very likely that this is already merged within two weeks. We have decided to use the official PHP SDK and possibly later look into Symfony client. So most likely these issues gets closes fairly soon and hopefully we have a MCP client that just works pretty soon.
Comment #5
marcus_johansson commentedComment #7
guignonvAwesome! Thanks! :-)
Comment #8
marcus_johansson commentedComment #9
marcus_johansson commentedNote that this is still their 0.6 version, meaning it is not stable. So we will only create beta versions for now, until they have a 1.0 version out.
Comment #10
marcus_johansson commentedSince the actual MCP Client doesn't really work at all at the moment with 11.3+, we will merge this.