Problem/Motivation

This is kind of a heisenbug and took some time to track down, but I have some understanding what is happening now. The problem is the following - when you try to save a mcp server with "fetch tools" on, you get the following error:

The website encountered an unexpected error. Try again later.

TypeError: Swis\McpClient\Client::listTools(): Return value must be of type Swis\McpClient\Results\ListToolsResult|Swis\McpClient\Results\JsonRpcError, null returned in Swis\McpClient\Client->listTools() (line 156 of /var/www/html/vendor/swisnl/mcp-client/src/ClientRequestTrait.php).

Independent on if its an update or a creation this happens.

If you add the data and click "Test Connection", the same problem does not occur. I have looked into MCPClient.php and where it's using listTools() method and I can not see any differences in how the client is loaded before it uses the listTools.

I have been able to track it down to only happening in Drupal 11.3.0, but if I revert to Drupal 11.2.0 its all working again.

Steps to reproduce

1. Install Drupal 11.3.0
2. Install the latest MCP Client module (or any)
3. Add a MCP Server and click save.

Proposed resolution

Figure out if there is some secondary dependency that got updated in Drupal 11.3.0 that swisnl/mcp-client or some other library is dependent on.

Issue fork mcp_client-3565919

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.

marcus_johansson’s picture

StatusFileSize
new49 KB

Both goes into McpClientFactory.php and run createFromEntity. The debug point there points to the exact same input, yet one fails and not the other.

Both runs listTools() directly after instantiation where one works and the other doesn't.

I can see also that the working swisnl/clients, whether that is the ajax call or Drupal 11.2 backend call, has two requestMaps, while the broken one only has one. The second one missing is of InitializedNotificationRequest. See screenshot from xdebug.

marcus_johansson’s picture

So in Drupal 11.2 afterInitialization inside SseTransporter is not being blocked, before the listTools is run in Drupal 11.3 on the backend save.

So you see the error render out on the page before the actual afterInitialization is run.

robertoperuzzo’s picture

I noticed this behaviour, I investigated without finding a real solution! For now, I suggest the workaround described in my comment #3565713-7: Support persistent sessions.

But I suspect there is something weird in using the SwissNL MCP client, we must go deeper on that and/or test other MCP client libraries like this one https://github.com/modelcontextprotocol/php-sdk

More details:

robertoperuzzo’s picture

I'm not sure but probably the Root Cause is the incompatibility between PHP Fibers (introduced in Drupal 11.3) and ReactPHP Event Loop (used by swissnl/mcp_client).

Drupal 11.3.0 introduced PHP Fibers for improved async performance. The problem is that the MCP Client module uses ReactPHP's await() function, which relies on PHP Fibers to handle promises. There's a timing/race condition between Drupal's Fiber management and ReactPHP's event loop.

robertoperuzzo’s picture

I implemented a roughly solution (see the MR) to fix the Fiber PHP breaking changes introduced with D11.3. I'm not sure that is the best solution but it seems to work.

I detailed the implementation in the FINAL_COMPLETE_SOLUTION.md file that can help for further improvements (we will remove it when the work will be completed).

Now I need some help by the community to understand if this is a correct path for the final implementation.

DISCLAIMER: I used AI assistant to help me in investiation and to write the documentation.

robertoperuzzo’s picture

Status: Active » Needs review
robertoperuzzo’s picture

Since Drupal 11.3 use Fibers and there is an open discussion in [Client] Implement PHP MCP Client Component in php-sdk and in the PR [Client] Feat: Implement MCP client component to use Fibers instead of external lib like ReactPHP, I was wondering if we can implement the event loop using Fibers.

I implemented, just for fun, a Fiber rough solution here. I tested using Slack MCP server (HTTP and STTDIO) and the communication seems to work, and it could be a starting point.

Some Benefits

For Users

  • ✅ Works seamlessly in Drupal 11.3+ Fiber context
  • ✅ No more "Connection aborted early" errors
  • ✅ No external dependencies to manage
  • ✅ Same API - no code changes needed

For Developers

  • ✅ Pure PHP - easier to debug
  • ✅ No ReactPHP learning curve
  • ✅ Simpler stack traces
  • ✅ Direct control over I/O

For Operations

  • ✅ Fewer dependencies in composer.json
  • ✅ Smaller vendor/ directory
  • ✅ Less attack surface
harivansh’s picture

Assigned: Unassigned » harivansh

harivansh’s picture

  • Imcp_client uses swisnl/mcp-client, which depends on ReactPHP promises and async waiting. Drupal 11.3 added Fiber based execution in rendering https://www.drupal.org/node/3392011, and that exposed a mismatch in how the client handled both connection setup and later request/response calls. So the problem was not only in the initial connect step. Calls like listTools() method after the connection was established were also affected.
  • I have added a Fiber-aware client wrapper so runtime MCP calls work correctly in Drupal 11.3 Fiber contexts. I also added a custom STDIO transporter and a process factory so STDIO uses the Drupal compatible transporter instead of the client default. I updated the main MCP client integration so both HTTP and STDIO use the Fiber aware client, updated the custom HTTP transporter so HTTP initialization also completes correctly before later MCP calls are made.
  • These changes were all needed together because the regression was happening in two places: first during transport initialization, and then again during later runtime MCP calls. Fixing only the transporterwas not enough.
harivansh’s picture

StatusFileSize
new153.4 KB

Test Preview:
MCP TEST - 1

harivansh’s picture

Assigned: harivansh » Unassigned
marcus_johansson’s picture

Assigned: Unassigned » marcus_johansson
marcus_johansson’s picture

Status: Needs review » Needs work

I've pushed a small change and now it works with 0.5.0 of the swisnl/mcp-client, but not with the 0.7.0 or 0.6.0 release.

Changes in the library: https://github.com/swisnl/mcp-client/compare/0.5.0...0.6.0.

robertoperuzzo’s picture

Sorry for my latency!

Thank you so much @harivansh for your contribution, I've reviewed it and it sounds good for me.

About the breaking change in swisnl/mcp-client from 0.5 to 0.6 reported by @marcus, I proposed a fix in the commit above. Check it out and tell me if it is fine for you both.

robertoperuzzo’s picture

Assigned: marcus_johansson » Unassigned
Status: Needs work » Needs review