Skip to main content
Glama
cswkim

mcp-use-hang-repro

by cswkim
README.md
# Repro: widget tool result is dropped when the handler loses a race with the iframe load

In `mcp-use dev`, a `tools/call` that renders a widget can hang in the inspector: the server
finishes the call, but the **result never reaches the client**, which spins until its request
timeout (`-32001`). Whether it hangs depends on how long the tool handler takes — it's a
**probabilistic race**, not a fixed timeout.

Measured on the stock scaffold by sweeping the handler delay (`DELAY_MS`):

| `DELAY_MS` | worked |
|-----------:|--------|
| 0          | always |
| 100        | always |
| 150        | ~6 / 8 |
| 200        | ~50%   |
| 250        | ~2 / 6 |
| 300        | never  |
| 1000       | never  |
| 2000       | never  |

The probability of hanging climbs smoothly from ~150ms to ~300ms — a transition band, not a
cliff. Reading: the tool result is racing the **widget iframe finishing its load/connect**
(itself a variable ~150–300ms). If the handler returns before the iframe is done, the result
is delivered; if it returns after, the result is dropped.

This is why it looks **intermittent** in a real server: a handler that hits an API/DB has
jittery latency that lands in the transition band, so it hangs only some fraction of the time.

- Only in **`mcp-use dev`**. A production build / deployed connector does **not** hang.
- This project is the **unmodified `create-mcp-use-app --template mcp-apps` scaffold**; the
  only change is making the stock `search-tools` 2s delay configurable via `DELAY_MS`.

## Environment

- mcp-use: **1.33.0** (also seen on 1.30.0). macOS, Chrome.

## Getting Started

```bash
npm install
npm run dev
```

## Steps

```bash
DELAY_MS=2000 npm run dev      # http://localhost:3000, opens the inspector
```

1. Open the inspector, call **`search-tools`** (args can be empty `{}`) → spins forever,
   eventually `-32001`. (At 2000ms it hangs every time.)
2. Restart with `DELAY_MS=0 npm run dev`, call again → delivers instantly, every time.
3. To see the race, sweep `DELAY_MS=150 / 200 / 250` (restart each time) and call the tool
   several times — it hangs a growing fraction of the time as the delay rises.

On hanging runs the widget iframe renders its skeleton (it received the tool *input*) but
never the *output* (props).

## Negative controls — Node clients do NOT reproduce it (so it's browser-specific)

These are MCP *clients*, so the dev server must already be running in another terminal.
`DELAY_MS` is a **server** setting (it controls the handler) — set it on `npm run dev`, not
on the script. Two terminals:

```bash
# terminal 1 — start the server at the hang-inducing delay, leave it running
DELAY_MS=2000 npm run dev

# terminal 2 — run a client against it
node repro.mjs          # SDK client: pre-read widget, then tools/call + resources/read CONCURRENTLY
node repro-flood.mjs    # SDK client: tools/call + a burst of widget asset fetches, concurrently
```

(If you see `ECONNREFUSED ::1:3000`, the dev server isn't running — start terminal 1 first.)

Both deliver the tool result fine even at `DELAY_MS=2000` (the browser hangs every time at
that delay). A headless `mcp-use client screenshot --tool search-tools` also works. These
never load the widget iframe in a browser, so there's nothing for the result to race —
consistent with the race reading above.

## Why (unconfirmed)

Browser-only + dev-only, probabilistic in a ~150–300ms band → looks like the result racing
something that completes when the widget iframe loads/connects, rather than a fixed timer or
plain connection contention. In `mcp-use dev` the iframe's assets are served from the **same
origin** (`localhost:3000`) by Vite; a deployed build serves prebuilt assets from a **separate
CDN origin** and doesn't hang. Exact mechanism unconfirmed.