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\CustomHeadersHttpTransporter extends Swis\McpClient\Transporters\StreamableHttpTransporter purely to inject custom HTTP headers, because the library offers no supported way to do it.
  • The same class overrides initializeConnection() to strip the library's register_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 \ReflectionMethod to call the private updateSessionIdFromResponse().
  • MCPClient::getStderrErrors() uses \ReflectionClass to read the client's protected transporter property just to surface STDIO stderr output on a failed connection.
  • MCPClient::listTools() and MCPClient::executeTool() hand-translate the library's result objects back into plain arrays, including method_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.5 to the official SDK, and re-evaluate whether minimum-stability: dev is 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. Delete CustomHeadersHttpTransporter entirely, along with its initializeConnection() override and the \ReflectionMethod call inside it.
  • Rebuild MCPClient::createStdioClient() on the SDK's STDIO transport. The SDK builds on Symfony Process, so command, env and cwd map across directly, and the $this->process reference 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 \ReflectionClass access to the protected transporter property 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() and executeTool() already return, replacing the method_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.md and the docs/ 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

Command icon 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

marcus_johansson created an issue. See original summary.

guignonv’s picture

Question: 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. :)

arianraeesi’s picture

marcus_johansson’s picture

@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.

marcus_johansson’s picture

Assigned: Unassigned » marcus_johansson

guignonv’s picture

Awesome! Thanks! :-)

marcus_johansson’s picture

Assigned: marcus_johansson » Unassigned
Status: Active » Needs review
marcus_johansson’s picture

Note 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.

marcus_johansson’s picture

Status: Needs review » Fixed

Since the actual MCP Client doesn't really work at all at the moment with 11.3+, we will merge this.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.