Skip to main content
Glama
vytharion

mcp-timeout-demo

by vytharion
README.md
# mcp-timeout-demo

A Claude Code MCP server that exposes a long-running tool with
**per-tool configurable timeouts** and periodic progress notifications,
so Claude Code never severs the request mid-flight.

If you have ever seen Claude Code kill an MCP tool call that was
otherwise healthy — because it went silent for longer than the client's
default idle window — this plugin shows the smallest possible fix and
ships it as a drop-in server you can install today.

Companion article:
<https://claudeplugins.nicedx.com/claude-code-mcp-tool-timeout-configurable-long-running/>

---

## What you get

- One MCP tool, `long_running_task`, with a `timeout_ms` parameter
  validated against `[1, 600_000]` ms.
- Cooperative cancellation via `asyncio.wait_for` — a genuinely stuck
  call fails at the ceiling, it does not hang forever.
- Progress notifications every 250 ms via `ctx.report_progress` — the
  Claude Code client's idle timer never fires while the tool is
  actually working.
- Per-tool timeout defaults from an env var **or** a JSON config file,
  with env vars winning over the file.
- A ready-to-copy `.mcp.json` snippet at
  [`examples/claude_code.mcp.json`](examples/claude_code.mcp.json).
- A Claude Code plugin manifest at
  [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) so the
  same server can install via a plugin marketplace.

---

## Install

The server ships as a standard Python distribution. Three install
paths, in order of preference:

### 1. `uv run` (recommended, zero pre-install)

`uv` bootstraps a venv on the fly, so nothing has to be pre-installed
globally. Drop this into your project's `.mcp.json`:

```json
{
  "mcpServers": {
    "mcp-timeout-demo": {
      "command": "uv",
      "args": ["run", "mcp-timeout-demo"],
      "env": {
        "MCP_TIMEOUT_DEMO_TIMEOUT_MS__LONG_RUNNING_TASK": "60000"
      }
    }
  }
}
```

Then restart Claude Code in that project. That is it.

### 2. `pip install` + console script

```bash
pip install mcp-timeout-demo
```

Register with Claude Code:

```bash
claude mcp add mcp-timeout-demo \
  --env MCP_TIMEOUT_DEMO_TIMEOUT_MS__LONG_RUNNING_TASK=60000 \
  -- mcp-timeout-demo
```

### 3. From source

```bash
git clone https://github.com/vytharion/claude-code-mcp-tool-timeout-configurable-long-running.git
cd claude-code-mcp-tool-timeout-configurable-long-running
uv sync
uv run mcp-timeout-demo
```

### 4. As a Claude Code plugin

The repository ships a `.claude-plugin/plugin.json` manifest, so the
server can also be published as a plugin through a Claude Code plugin
marketplace. The `.claude-plugin/marketplace.json` in this repo is the
minimal example marketplace entry — point your marketplace tooling at
the repository URL and Claude Code will read both files.

---

## Configure

The tool has one default: `DEFAULT_TIMEOUT_MS = 5_000`. Two mechanisms
override that default, in this precedence (most-specific wins):

### Environment variable

```
MCP_TIMEOUT_DEMO_TIMEOUT_MS__<TOOL_NAME_UPPER>
```

Example — raise the ceiling for `long_running_task` to 60 seconds:

```bash
export MCP_TIMEOUT_DEMO_TIMEOUT_MS__LONG_RUNNING_TASK=60000
```

### JSON config file

Set `MCP_TIMEOUT_DEMO_CONFIG` to point at a JSON file with this shape:

```json
{
  "tools": {
    "long_running_task": {
      "timeout_ms": 60000
    }
  }
}
```

Then:

```bash
export MCP_TIMEOUT_DEMO_CONFIG=/path/to/mcp-timeouts.json
```

### Per-call override

Clients can still pass `timeout_ms` on the individual `call_tool`
request. That per-call value always wins over the default — the
default only sets the ceiling the schema advertises.

---

## Verify

Once the server is registered, ask Claude Code to call the tool:

> "Call `long_running_task` with label 'demo', `timeout_ms=3000`, and
> `work_duration_ms=1500`."

You should see the tool return:

```
done:demo:worked=1500ms:timeout=3000ms
```

To see the timeout path fire, ask for work that exceeds the deadline:

> "Call `long_running_task` with label 'over', `timeout_ms=200`, and
> `work_duration_ms=5000`."

The tool returns cleanly with:

```
timeout:over:after=200ms
```

The client will also receive periodic progress notifications during
either call.

---

## Troubleshooting

### "Claude Code still says the tool timed out"

That is the *client*-side MCP timeout, not the tool's `timeout_ms`.
This plugin only fixes the *server-side* idle-timer story via progress
notifications. If Claude Code itself has a hard per-call ceiling, this
plugin cannot reach past it — file a follow-up config with the client.

### "The env var is not being picked up"

The env-var key encodes the tool name in **upper case with underscores
preserved**. `long_running_task` becomes
`MCP_TIMEOUT_DEMO_TIMEOUT_MS__LONG_RUNNING_TASK` (note the double
underscore between prefix and tool name). Print `env_key_for(tool_name)`
from the `mcp_timeout_demo.config` module if you are unsure.

### "The tool default advertised in `tools/list` is wrong"

The advertised default is captured at server startup. If you change
the env var or the config file, restart the MCP server (in Claude
Code: `/mcp` → restart the server, or just restart the client).

### "I get `FileNotFoundError: config file not found`"

`MCP_TIMEOUT_DEMO_CONFIG` must point at an existing file; a missing
path is treated as operator error, not silently ignored. Check the
path and the process's working directory.

### "I get `ValueError: timeout ... outside [1, 600000]`"

Timeout values are clamped to `[1, 600_000]` ms (10 minutes). A
malformed override fails loudly at startup rather than silently
degrading — fix the value in your env or config file.

---

## Upgrade notes

- Semantic Versioning applies. Minor releases may add tools or config
  keys; patch releases are strictly bug fixes.
- The env-var **prefix** (`MCP_TIMEOUT_DEMO_`) and **key template**
  (`MCP_TIMEOUT_DEMO_TIMEOUT_MS__<TOOL_NAME_UPPER>`) are part of the
  public API. They will not change inside a major version.
- The JSON config file schema (`tools.<name>.timeout_ms`) is also
  part of the public API for the current major.

See [CHANGELOG.md](CHANGELOG.md) for the full release history.

---

## Development

```bash
uv sync
uv run pytest -v
```

The suite drives the server through the SDK's in-memory transport, so
no subprocess or stdio hand-off is required to run it.

---

## License

MIT — see [LICENSE](LICENSE).

TDQS

C2.1/5.0

Scored across 1 tool

Disambiguation5/5

Only one tool exists, so there is no possibility of confusion or overlap. Every available tool has a distinct purpose by virtue of being the sole tool.

Naming Consistency5/5

With a single tool, naming consistency is trivially satisfied. The name 'long_running_task' is clear and follows a descriptive pattern, although no pattern can be established from one example.

Tool Count3/5

A single tool feels thin for general use, but it is borderline acceptable for a server explicitly named 'mcp-timeout-demo' whose purpose appears to be demonstrating timeout behavior. One tool may be intentionally minimal for this narrow scope.

Completeness5/5

For a server dedicated to timeout demonstration, the 'long_running_task' tool fully covers the apparent purpose. There are no obvious gaps or dead ends given the focused domain.

Maintenance

ActivitySlowing
ResponsivenessNo issues