Metadata-Version: 2.4
Name: warmcp
Version: 0.2.0
Summary: Warm-start MCP launcher that hot-swaps its tool list to survive Windows cold-start timeouts
Author-email: Newton Winter <isoutthere@gmail.com>
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: fastmcp>=3.4.2
Requires-Dist: psutil>=5.9
Description-Content-Type: text/markdown

# warmcp

A transparent **warm-start gateway** for slow-to-boot MCP servers.

On Windows, Claude Desktop hard-kills an MCP server that doesn't come up within
~60 s. A server launched as `uvx some-pkg@latest` routinely blows that budget on
a cold start (downloading/building dependencies) and is killed before it serves
a single tool.

warmcp wraps such a server:

1. It **starts instantly** and exposes a `warmcp_status` tool.
2. It boots the real child (`uvx <your args>`) in the **background**.
3. The moment the child is ready, it **mirrors the child's entire surface**
   (tools + resources + prompts) verbatim and notifies the client via
   `notifications/*_list_changed` — so the client picks them up with no
   reconnect.
4. If the child fails to start, hangs, or crashes, the gateway degrades to a
   `warmcp_crash_report` tool instead of vanishing.

The tool list refresh is standard MCP (`tools.listChanged`) and is validated
against real clients (Claude Code / Claude Desktop).

## Usage

Wrap any `uvx`-launched server by inserting `warmcp@latest` before it. The rest
of the arg line becomes the child command under `uvx`:

```jsonc
{
  "mcpServers": {
    "just-prs": {
      "command": "uvx",
      "args": ["warmcp@latest", "just-prs-mcp@latest", "stdio"],
      "env": { "PRS_MCP_MODE": "essentials", "MCP_TIMEOUT": "300000", "PYTHONUNBUFFERED": "1" }
    }
  }
}
```

`warmcp` inherits and passes the full environment through to the child, so the
child's own vars (`PRS_MCP_MODE`, …) work unchanged.

Local (pre-publish) form, running from a checkout:

```jsonc
{
  "mcpServers": {
    "just-prs": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/warmcp", "warmcp", "just-prs-mcp@latest", "stdio"]
    }
  }
}
```

## States

| State        | Surface (prod)                       | + with `WARMCP_DEBUG`                          |
|--------------|--------------------------------------|------------------------------------------------|
| starting     | `warmcp_status`                      | `warmcp_stdout`, `warmcp_stderr`               |
| ready        | the child's tools/resources/prompts  | + `warmcp_stdout`, `warmcp_stderr`, `warmcp_metrics` |
| restarting   | `warmcp_status`                      | + `warmcp_stdout`, `warmcp_stderr`, postmortem `warmcp_crash_report` |
| dead         | `warmcp_crash_report`                | + `warmcp_stdout`, `warmcp_stderr`             |

"Ready" means the child initialized **and** its tools/resources/prompts could be
listed — not merely that it answered the MCP handshake.

## Environment

| Var | Default | Meaning |
|-----|---------|---------|
| `WARMCP_TIMEOUT` | `600000` | ms; max time for **one** start attempt to become ready. Halting-problem guard, not a fixed timer. |
| `WARMCP_DEBUG` | off | Expose `warmcp_stdout`/`warmcp_stderr` (and `warmcp_metrics` once ready) alongside the child; surface crash diagnostics even when a restart recovers. |
| `WARMCP_RESTART_ON_FAILURE` | `0` | Applies to **post-ready** crashes only. `0` = unlimited restart, `N>0` = up to N attempts then crash_report, `<0` = report-only. A *failed start* is always terminal regardless. |
| `WARMCP_CHILD_COMMAND` | `uvx` | Command used to spawn the child (override for local testing). |

## Develop / test

```bash
uv sync
uv run pytest -s        # end-to-end tests over real stdio with a dummy child
uv run ruff check .
```
