DEV Community

Cover image for What Happens When an MCP Tool Call Never Returns?
L Anil Kumar Singha
L Anil Kumar Singha

Posted on

What Happens When an MCP Tool Call Never Returns?

A normal MCP tool call is straightforward: the client sends a request, the server runs the tool, and a response comes back.

A hang is different. The server accepts the call but never returns a result or an error.

I wanted to test how an MCP client behaves in that case without relying on network tricks or manually killing a process.

Reproducing a hang

MCP Failure Lab has a hang tool specifically for this.

The behavior is intentionally simple: once called, it doesn't resolve.

That gives us a repeatable failure instead of trying to approximate one with a very long delay.

A scenario for it looks like this:

{
  "name": "hung tool call times out",
  "call": {
    "tool": "hang",
    "args": {}
  },
  "timeoutMs": 1000,
  "expect": {
    "outcome": "timeout"
  }
}
Enter fullscreen mode Exit fullscreen mode

The server behavior and the expected client behavior are separate here.

The server hangs.

The client is expected to time out after one second.

That distinction matters because a hung server doesn't produce a timeout by itself. The timeout has to be enforced by the client or by something around the request.

Hang vs delay

I originally treated a long delay as being close enough to a hang, but they're useful for testing different things.

A delayed call still has a completion point:

request ---- 5 seconds ----> response
Enter fullscreen mode Exit fullscreen mode

A hung call doesn't:

request -------------------->
        -------------------->
        -------------------->
Enter fullscreen mode Exit fullscreen mode

With a delay, a response can still arrive after the client has timed out.

With a hang, there is no eventual response.

That difference starts to matter when testing cancellation and cleanup.

A timeout only tells you what the client saw

Suppose a client calls a tool and gets a timeout.

It's tempting to treat that as meaning the operation failed.

That's not necessarily true.

Consider a tool that changes some state:

client                    server
   |                         |
   | ---- request ---------->|
   |                         | changes state
   |                         |
   |       response lost     X
   |
   | ---- timeout
Enter fullscreen mode Exit fullscreen mode

The client saw a timeout, but the server may have already completed the operation.

If the client automatically retries, the operation might run twice.

This is one reason I'm interested in keeping the observed outcome separate from server state in Failure Lab.

It's also where simple timeout testing stops being enough.

What should happen after the timeout?

There are a few things worth checking beyond whether a timeout was thrown:

  • Is the original operation cancelled?
  • Is the MCP session still usable?
  • Can another tool call succeed?
  • Does the client retry?
  • Can you determine whether the original operation changed state?

The last one is particularly useful for tools with side effects.

Failure Lab supports an independent observe call for scenarios where state needs to be checked after the main call.

That lets a test distinguish between:

client observed: timeout
server state: unchanged
Enter fullscreen mode Exit fullscreen mode

and:

client observed: timeout
server state: changed
Enter fullscreen mode Exit fullscreen mode

Those are very different outcomes even though the client reported the same error.

Keeping the failure deterministic

The main reason I built the hang fault wasn't to simulate an unreliable network.

It was to remove the unreliable part from the test.

If the server hangs deterministically, I can run different clients against the same behavior and compare what they do.

The path still goes through MCP:

scenario
   ↓
MCP client
   ↓
transport
   ↓
MCP server
   ↓
hang
Enter fullscreen mode Exit fullscreen mode

Only the failure is controlled.

That makes bugs around timeout handling much easier to reproduce.

Try it

The project is open source:

npx mcp-failure-lab demo
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/anilloutombam/mcp-failure-lab

I'm working through other failure cases as well, particularly cancellation, session loss, malformed responses, and cases where the client reports failure even though the server changed state.

If you've hit an MCP failure that was difficult to reproduce, open an issue. I'd rather turn real failure cases into deterministic scenarios than invent them.

Top comments (0)