# An MCP server can vanish from your AI agent mid-conversation. Here's the 30-second timeout that did it to me.

> Source: <https://dev.to/achiya-automation/an-mcp-server-can-vanish-from-your-ai-agent-mid-conversation-heres-the-30-second-timeout-that-did-3khe>
> Published: 2026-05-28 08:56:52+00:00

The bug report was: "the browser tools are gone."

I'd been running the same Claude Code session for an hour, calling `safari_navigate`

, `safari_click`

, `safari_read_page`

— the usual flow. Then I opened a new conversation in the same project and the safari tools weren't in the catalog at all. The agent didn't say "I tried to use safari-mcp and it's not available." It just… didn't use them. It re-implemented half of what I needed with `Bash`

and `curl`

.

That second part is the worst part. The agent doesn't *know* that the tool catalog is incomplete. It only knows what's in front of it. If a tool is missing, it makes do with what it has — and the user has no idea their last release broke discoverability.

This post is about the 30-second timeout that caused it, the diagnosis path, and the one-line fix. But more than that, it's about a failure mode in stdio MCP that I think every MCP author needs to know about and most don't.

`safari-mcp`

is an MCP server that drives the real macOS Safari. When the user wants their agent to use a separate browser profile (e.g. "Work" vs "Personal"), they launch the server with `SAFARI_PROFILE=work`

and the server scopes every tool call to that profile's window. That means at startup the server has to find the window — call AppleScript, enumerate Safari's open windows, match by profile name, cache the window ref.

Here's what the startup code used to do:

``` js
if (SAFARI_PROFILE) {
  await new Promise(r => setTimeout(r, 50));
  await refreshTargetWindow(true);   // <-- this line
  if (_targetWindowRef) {
    _logProfile(`Startup: Profile "${SAFARI_PROFILE}" → ${_targetWindowRef}`);
  } else {
    _logProfile(`WARNING: Profile "${SAFARI_PROFILE}" window NOT found`);
  }
}
```

ES module top-level await. Looks fine. Profile detection runs once, the server knows which window to target, life is good.

In testing this took ~50–200ms. In production it sometimes took longer than 30 seconds.

When Claude Code launches an MCP server it expects an `initialize`

response within 30 seconds. That's the handshake — the server announces its protocol version and tool catalog, the client says "ok, here's my session." Until that handshake completes, the server's tools don't enter the conversation's tool catalog.

If your top-level await runs `>30s`

before the stdio loop gets a chance to respond, the handshake misses the deadline. The client gives up. The server is killed. No retry. No warning surfaced to the user, just a log entry deep in the Claude Code internals that says "MCP server failed to initialize in 30s."

And critically: **the conversation continues**. The agent's tool catalog is whatever responded in time. Safari tools just aren't there. The agent has no way to know they were *supposed* to be there.

I want to underline this: the failure was completely invisible to me as a user. I didn't see a stack trace. I didn't see a "your tools didn't load." I saw an agent that didn't reach for the tools I'd just shipped a fix for.

`refreshTargetWindow(true)`

calls into a Swift helper that runs:

```
tell application "Safari"
  return name of every window
end tell
```

On a fresh Safari with three tabs this returns in 12ms. On a real user's Safari, it does any of the following:

`name of every window`

waits for each window's title to settle. 5–20s.`~/Library/Containers/com.apple.Safari/`

. The TCC privacy subsystem reverifies your bundle's automation permission. Anywhere from instant to "until the user moves their mouse."None of those are bugs. They are normal macOS behavior. They were not in the 99th percentile when I tested — they showed up in the 99.9th percentile when the server hit my actual user base.

The first thing I did was assume the bug was in the MCP protocol layer. I went and looked at the stdio framing code, the JSON-RPC parser, the request dispatcher. None of it was the problem.

The second thing I did was look at the `refreshTargetWindow`

call and think "well, it works in my testing." Which is the most expensive sentence in software.

The actual diagnostic, which took me about 20 minutes to find, was to read the Claude Code MCP debug logs:

```
[MCP] safari-mcp: spawned (pid 47192)
[MCP] safari-mcp: sending initialize request
[MCP] safari-mcp: initialize timed out after 30000ms, killing process
```

That's it. That's the only signal. The MCP client doesn't tell you *what* the server was doing. It doesn't ask the server "are you stuck?" It just kills it.

Once I had that line, the rest was obvious: the only thing that runs before stdio is `refreshTargetWindow`

. If `refreshTargetWindow`

is slow, stdio never gets a chance. Therefore: don't block stdio on it.

``` js
if (SAFARI_PROFILE) {
  (async () => {
    await new Promise(r => setTimeout(r, 50));
    await refreshTargetWindow(true);
    if (_targetWindowRef) {
      _logProfile(`Startup: Profile "${SAFARI_PROFILE}" → ${_targetWindowRef}`);
    } else {
      _logProfile(`WARNING: Profile "${SAFARI_PROFILE}" window NOT found`);
    }
  })();
}
```

Wrap the whole startup probe in a fire-and-forget IIFE. Module init returns immediately. Stdio loop binds. Initialize handshake responds in ~5ms. By the time the first `safari_*`

tool call arrives, the profile window probe has usually finished — and if it hasn't, `getTargetWindowRef()`

already has a lazy-refresh path that handles a missing cache by running the probe inline.

The correctness story is: the probe is a cache warm-up, not a hard prerequisite. The tool call path already knows how to handle a cold cache. So there is no reason to make module init wait.

Three lines changed. The bug is gone.

If your MCP server does *anything* at startup that touches an external process, an external API, the filesystem outside your bundle, or a system service, **you cannot let it block initialize**.

`tools/list`

or first tool call, not at import.`xprop`

? You have no SLA on those. Defer.`~/.config/<your-tool>`

file? Pretty safe. But still: if it's gone or corrupted, log and continue; don't crash module init.The asymmetric cost matters here. If your slow probe blocks `initialize`

, the failure mode is the worst possible kind: silent absence of your tools, no error surfaced, agent doesn't know to retry. If your slow probe runs in the background and a tool call arrives before it finishes, the failure mode is at most a single slow tool call — and you can return a clear error message that the agent can see and act on.

There is no version of this trade where blocking is the right answer.

This bug shouldn't be possible to ship. Some specific changes I'd love to see:

`initialize`

should not block on tool catalog completeness.`tools/list?ttl=lazy`

, ask me later."`package.json`

will be unavailable this session." The agent reads that, the user reads that, everyone can act on it.`initialize`

timeouts should generate a retry, not a kill.None of those are the server author's problem to fix, but all of them would have caught this bug for me before a user did.

We are heading toward agents that ship with dozens of MCP servers. The probability that at least one of them silently fails to initialize on any given launch goes from "small" to "nearly certain" once you're stacking 10+ servers. If the failure mode is "agent silently lacks tools it should have," the user experience for AI agents becomes "sometimes the AI is dumber than usual and we don't know why."

That's not a future I want. It's a future where users blame the model for missing capabilities that the harness didn't even surface.

If you're shipping an MCP server: audit your top-level await. If it touches anything that could stall, move it off the critical path. Today. Before someone files the bug report I just filed against myself.

*The fix shipped in safari-mcp v2.11.9 today. The full diff is here. The project is on GitHub if you want to see how an MCP server scopes itself to a Safari profile, or to file the next bug.*
