mcp-nap 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_nap-0.2.0/.gitignore +7 -0
- mcp_nap-0.2.0/LICENSE +21 -0
- mcp_nap-0.2.0/PKG-INFO +211 -0
- mcp_nap-0.2.0/README.md +196 -0
- mcp_nap-0.2.0/bench/measure.py +429 -0
- mcp_nap-0.2.0/mcp_nap.py +676 -0
- mcp_nap-0.2.0/pyproject.toml +34 -0
- mcp_nap-0.2.0/test_mcp_nap.py +1110 -0
mcp_nap-0.2.0/.gitignore
ADDED
mcp_nap-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ashutosh Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mcp_nap-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-nap
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Keeps stdio MCP servers asleep until a tool is called, so Claude Code, Cursor and VS Code stop holding idle copies.
|
|
5
|
+
Author: Ashutosh Sharma
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: claude-code,cursor,mcp,model-context-protocol,vscode
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# mcp-nap
|
|
17
|
+
|
|
18
|
+
Your MCP servers sleep until you call a tool.
|
|
19
|
+
|
|
20
|
+
Claude Code, Cursor and VS Code each start their own copies of every stdio MCP server long before anything calls one, and keep them running: Claude Code one set per session, Cursor as soon as a window opens, VS Code one set per chat. mcp-nap stands in for each server, answers the client's startup requests from a cache, and starts the real server only when one of its tools is called. After 10 quiet minutes it stops it again. The model sees the same tool list either way; the saving is the idle node processes and their memory.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install mcp-nap
|
|
24
|
+
mcp-nap wrap # Claude Code
|
|
25
|
+
mcp-nap wrap --client cursor # Cursor
|
|
26
|
+
mcp-nap wrap --client vscode # VS Code
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`wrap` rewrites every stdio server in that client's config to run through mcp-nap and prints where it saved a backup first. Name servers to wrap only those: `mcp-nap wrap playwright shadcn`. What to do after `wrap` depends on the client, below. Servers that hold state lose it at the 10-minute stop: Playwright's open browser is the usual one, see [Servers that keep state](#servers-that-keep-state).
|
|
30
|
+
|
|
31
|
+
To try it without installing:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx mcp-nap wrap
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That works, but uvx stays running as the parent of every wrapped server, so each one costs about 56 MB instead of 22 MB. With four servers that is 223 MB per session instead of 89 MB. It also writes `uvx mcp-nap` into the config with no version, so which release runs is up to uv's cache at each start. Install it with `uv tool install` (or `pipx install mcp-nap`) once you keep it.
|
|
38
|
+
|
|
39
|
+
## Let your agent set it up
|
|
40
|
+
|
|
41
|
+
Paste this to Claude Code, Cursor, VS Code chat or any agent that can run commands on your machine:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
Set up mcp-nap (https://github.com/ashuwhy/mcp-nap) so my stdio MCP servers sleep until a tool is called.
|
|
45
|
+
|
|
46
|
+
1. If `uv --version` fails, install uv the way https://docs.astral.sh/uv/getting-started/installation/ says. Run `uv tool install mcp-nap`, then `mcp-nap --help`. If mcp-nap is not found, run `uv tool update-shell` and use a new shell.
|
|
47
|
+
2. Check which of these I use and list the stdio servers (entries with a "command") in each. Read the files only; never print env values or headers, they often hold API keys.
|
|
48
|
+
- Claude Code: `claude` is on PATH; top-level "mcpServers" in ~/.claude.json
|
|
49
|
+
- Cursor: "mcpServers" in ~/.cursor/mcp.json
|
|
50
|
+
- VS Code: "servers" in ~/Library/Application Support/Code/User/mcp.json (macOS), %APPDATA%\Code\User\mcp.json (Windows) or ~/.config/Code/User/mcp.json (Linux)
|
|
51
|
+
3. Show me the list and ask which to wrap. Flag servers that keep state between calls (Playwright and other browser servers) and suggest --idle 0 for them.
|
|
52
|
+
4. Wrap with mcp-nap only, never by editing a config yourself:
|
|
53
|
+
Claude Code: mcp-nap wrap NAME...
|
|
54
|
+
Cursor: mcp-nap wrap NAME... --client cursor
|
|
55
|
+
VS Code: mcp-nap wrap NAME... --client vscode
|
|
56
|
+
Wrap the stateful ones in a separate run with --idle 0 added.
|
|
57
|
+
5. Tell me what each run printed (backup path, any note, the restart step), and that `mcp-nap unwrap` with the same --client undoes it.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Clients
|
|
61
|
+
|
|
62
|
+
Checked against Claude Code 2.1, Cursor 3.29 and VS Code 1.146 on macOS. All three change how they run servers often.
|
|
63
|
+
|
|
64
|
+
### Claude Code
|
|
65
|
+
|
|
66
|
+
Every session starts every stdio server and keeps it until the session ends, so five open sessions hold five idle copies of each. `mcp-nap wrap` edits the user servers in `~/.claude.json` (or `$CLAUDE_CONFIG_DIR/.claude.json`) through `claude mcp`, because live sessions write that file too. Then:
|
|
67
|
+
|
|
68
|
+
> Restart open Claude Code sessions to pick this up.
|
|
69
|
+
|
|
70
|
+
### Cursor
|
|
71
|
+
|
|
72
|
+
As soon as a signed-in window opens, Cursor starts every enabled server, before any chat, and keeps it until you quit. A second window or workspace can add more copies. `mcp-nap wrap --client cursor` edits `~/.cursor/mcp.json` (`%USERPROFILE%\.cursor\mcp.json` on Windows). Then:
|
|
73
|
+
|
|
74
|
+
> Cursor starts the new command by itself, but copies already running keep the old one: toggle each changed server off and on in Cursor Settings > Tools & MCP, or quit and reopen Cursor.
|
|
75
|
+
|
|
76
|
+
Project `.cursor/mcp.json` files are not touched.
|
|
77
|
+
|
|
78
|
+
### VS Code
|
|
79
|
+
|
|
80
|
+
Chat in VS Code 1.146 runs on the Copilot agent host, which starts a full set of servers for every chat session and keeps it until you delete the chat or quit. Archiving a chat doesn't stop its set. Opening an empty new chat starts one more, dropped after 5 minutes if you never type. Wrapped, every one of those copies is a sleeping 22 MB wrapper, and the real server runs only around a tool call.
|
|
81
|
+
|
|
82
|
+
`mcp-nap wrap --client vscode` edits the default profile's `mcp.json`: `~/Library/Application Support/Code/User/mcp.json` on macOS, `%APPDATA%\Code\User\mcp.json` on Windows, `~/.config/Code/User/mcp.json` on Linux (or under `$XDG_CONFIG_HOME`). Then:
|
|
83
|
+
|
|
84
|
+
> VS Code picks up the new command in each chat the next time you send a message in it, or quit and reopen VS Code.
|
|
85
|
+
|
|
86
|
+
- A server with `"sandboxEnabled": true` is left alone: the sandbox would not let mcp-nap write its cache.
|
|
87
|
+
- On the older Local harness (`chat.defaultToCopilotHarness` off), VS Code already starts servers on first use, so mcp-nap only adds the idle stop. That harness also needs `rg` (ripgrep) on PATH to start any sandboxed server, wrapped or not; without it the server exits with `ripgrep (rg) not found`.
|
|
88
|
+
- Other profiles, VS Code Insiders and workspace `.vscode/mcp.json` files are not touched.
|
|
89
|
+
|
|
90
|
+
### Cursor and VS Code config files
|
|
91
|
+
|
|
92
|
+
`wrap` and `unwrap` write the file the way it was laid out (indent, line endings, key order), so `unwrap` gives back the same bytes. A file laid out by hand gets reformatted once, with a note that the backup has it as it was. A file with comments or trailing commas is refused and left as it is, since those would be lost.
|
|
93
|
+
|
|
94
|
+
### Any other client
|
|
95
|
+
|
|
96
|
+
mcp-nap is a plain stdio server, so any client that runs stdio servers can use it. Put it in front of the server's command:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
"playwright": {
|
|
100
|
+
"command": "mcp-nap",
|
|
101
|
+
"args": ["playwright", "--", "npx", "-y", "@playwright/mcp@latest"]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The first argument names the cache file, so give each server its own. If the client doesn't see your shell's PATH, use the full path that `command -v mcp-nap` prints. (`wrap` already writes the full path.)
|
|
106
|
+
|
|
107
|
+
## What it saves
|
|
108
|
+
|
|
109
|
+
With four servers, one session held 948 MB bare and 89 MB wrapped.
|
|
110
|
+
|
|
111
|
+
Measured on my laptop with four npx servers (mui, shadcn, playwright, chrome-devtools) per session. A session here is what Claude Code does at startup: spawn every server, initialize, list tools, then sit idle. In Cursor and VS Code, count one session per set of copies the client starts.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
machine: Darwin arm64, 16 GB RAM, 6026 MB free at start, Python 3.15.2, node v22.16.0
|
|
115
|
+
uv 0.10.2, uv tool install picked Python 3.13.5
|
|
116
|
+
env: npm_config_prefer_offline=true npm_config_update_notifier=false CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1 CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Resident memory, 4 servers per session, wrapper cache warm:
|
|
120
|
+
|
|
121
|
+
| sessions | bare | mcp-nap (uv tool install) | mcp-nap (python3 mcp_nap.py) | mcp-nap (uvx) |
|
|
122
|
+
|---|---|---|---|---|
|
|
123
|
+
| 1 | 948 MB (948 MB/session) | 89 MB (89 MB/session) | 75 MB (75 MB/session) | 222 MB (222 MB/session) |
|
|
124
|
+
| 3 | 2635 MB (878 MB/session) | 268 MB (89 MB/session) | 228 MB (76 MB/session) | 668 MB (223 MB/session) |
|
|
125
|
+
| 5 | 3836 MB (767 MB/session) | 447 MB (89 MB/session) | 379 MB (76 MB/session) | 1115 MB (223 MB/session) |
|
|
126
|
+
|
|
127
|
+
- mcp-nap (uv tool install) wrapper: 22.4 MB per wrapped server
|
|
128
|
+
- mcp-nap (python3 mcp_nap.py) wrapper: 19.0 MB per wrapped server
|
|
129
|
+
- mcp-nap (uvx) wrapper: 55.7 MB per wrapped server, uvx stayed as parent in 20 of 20
|
|
130
|
+
- memory pressure at 3 sessions, bare: the compressor grew 486 MB while measuring, so that cell reads up to that much low
|
|
131
|
+
- memory pressure at 5 sessions, bare: the compressor grew 826 MB while measuring, so that cell reads up to that much low
|
|
132
|
+
|
|
133
|
+
Delay, median of 3 runs (nap = python3 mcp_nap.py; cold = no mcp-nap cache yet, asleep = warm cache, server not started):
|
|
134
|
+
|
|
135
|
+
| server | bare start | nap start cold | nap start warm | uv tool start warm | uvx start warm | bare call | nap call asleep |
|
|
136
|
+
|---|---|---|---|---|---|---|---|
|
|
137
|
+
| mui | 515 ms | 517 ms | 34 ms | 33 ms | 73 ms | 6 ms | 541 ms |
|
|
138
|
+
| shadcn | 536 ms | 531 ms | 33 ms | 28 ms | 69 ms | 8 ms | 504 ms |
|
|
139
|
+
| playwright | 523 ms | 532 ms | 34 ms | 29 ms | 72 ms | 927 ms | 1.1 s |
|
|
140
|
+
| chrome-devtools | 593 ms | 600 ms | 33 ms | 28 ms | 68 ms | 12 ms | 568 ms |
|
|
141
|
+
|
|
142
|
+
The bare cells at 3 and 5 sessions were measured while macOS was compressing pages, so they read low and the saving there is larger than shown. The 1-session row had no pressure.
|
|
143
|
+
|
|
144
|
+
The delay is paid on the first call to a server after it went to sleep, then calls run at the bare speed until it idles out. Startup gets faster, because the tool list comes from the cache instead of a fresh node process.
|
|
145
|
+
|
|
146
|
+
To rerun it on your machine (needs uv and node, about 5 minutes):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python3 bench/measure.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It builds the wheel from the checkout, installs it with `uv tool install` into a temp dir, uses its own temp cache, and skips a session count when there isn't free memory for it. `--sessions 1,3` and `--reps 1` make it shorter; `--skip-uv` runs only bare and `python3 mcp_nap.py`.
|
|
153
|
+
|
|
154
|
+
RSS counts shared pages (the node binary, libc) once per process, so every column reads high. Read the difference between columns, not the absolute numbers.
|
|
155
|
+
|
|
156
|
+
## How it works
|
|
157
|
+
|
|
158
|
+
The first time a wrapped server runs, mcp-nap starts it, passes everything through, and saves its `initialize`, `tools/list`, resource and prompt list replies under `~/.cache/mcp-nap` (`$XDG_CACHE_HOME` if set). After that, a new session gets those replies from the cache and the server stays off. Resource subscriptions and the log level, which Cursor and VS Code send on connect, are taken by mcp-nap and handed to the server when it starts. The first other request, usually a tool call, starts it. It stops again after 10 minutes with nothing in flight.
|
|
159
|
+
|
|
160
|
+
If the server's command or its entry script changes, the cache is refilled on the next start. If the lists change anyway, mcp-nap sends `list_changed` and the client refetches.
|
|
161
|
+
|
|
162
|
+
## What sleeping costs
|
|
163
|
+
|
|
164
|
+
- The first call after a stop waits for the server to start, about half a second for the npx servers above.
|
|
165
|
+
- No completions while asleep: `completion/complete` gets an empty list until something wakes the server.
|
|
166
|
+
- No resource updates while asleep. Subscriptions are kept and passed on at the next start, but nothing is running to notice a change in between.
|
|
167
|
+
- State the server holds dies at the stop, below.
|
|
168
|
+
|
|
169
|
+
## Servers that keep state
|
|
170
|
+
|
|
171
|
+
A server that holds state between calls loses it when it stops. Playwright's open browser is the common one. Keep those running for the life of the session with `--idle 0`:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
mcp-nap unwrap playwright && mcp-nap wrap playwright --idle 0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--idle N` sets any other timeout in seconds.
|
|
178
|
+
|
|
179
|
+
## Servers whose login expires
|
|
180
|
+
|
|
181
|
+
Some servers keep an OAuth token that dies on a schedule, and every call then fails with `invalid_grant` until you run the server's own login command. `--reauth ARG` does that for you: on such a failure it runs `<command> ARG`, opens the first https URL it prints, and restarts the server once the login exits cleanly. `wrap` doesn't set it, so add it by hand:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
"google-docs": {
|
|
185
|
+
"command": "mcp-nap",
|
|
186
|
+
"args": ["google-docs", "--reauth", "auth", "--", "google-docs-mcp"]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Undo
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
mcp-nap unwrap # or: unwrap --client cursor, unwrap --client vscode
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
That puts every wrapped server back as it was. Env edits you made to a wrapped entry after `wrap` are kept. Or quit the client and copy the backup back over the config. `wrap` printed its path; it sits next to the config as `.claude.json.mcp-nap-<date>-<time>` or `mcp.json.mcp-nap-<date>-<time>`.
|
|
197
|
+
|
|
198
|
+
## http and sse servers
|
|
199
|
+
|
|
200
|
+
They are left alone in every client. For Claude Code, mcp-nap edits the config through `claude mcp remove` and `claude mcp add-json`, because live sessions write `~/.claude.json` too, and removing an http or sse server would also delete its OAuth login. Re-adding a server moves it to the end of the list, so if an http server sits between stdio servers, the stdio servers after it end up after it in the file. `wrap` prints a note when that happens. The order doesn't change how Claude Code runs them.
|
|
201
|
+
|
|
202
|
+
## Limits
|
|
203
|
+
|
|
204
|
+
- macOS, Linux and Windows. The tables above are from macOS. On Linux (a Debian container, node 22, Python 3.11) the tests pass and one `bench/measure.py --sessions 1 --reps 1` run gave 841 MB bare against 42 MB with `uv tool install`.
|
|
205
|
+
- On Windows 11 (arm64, Python 3.9 and 3.13, node 22) the tests pass, and a wrapped `npx` filesystem server starts on the first tool call and leaves no `cmd.exe` or `node.exe` behind after the idle stop, after the session closes, or after the wrapper is killed. Not yet tried there: `wrap` against a real Claude Code install, and Claude Code starting the `mcp-nap` console script. If `claude` is npm's `claude.cmd`, cmd.exe may expand a `%NAME%` inside an env value as `wrap` passes it through.
|
|
206
|
+
- `--client cursor` and `--client vscode` were run against the real apps on macOS only. Their tests pass on Linux and Windows too.
|
|
207
|
+
- Stdio servers only.
|
|
208
|
+
- Without `--client`, `wrap` needs the `claude` CLI on PATH.
|
|
209
|
+
- Without `--client`, `wrap` and `unwrap` pass each server's entry, env values included, to `claude mcp add-json` as a command-line argument. For about a second per server, another user on the same machine can read it in `ps`.
|
|
210
|
+
|
|
211
|
+
MIT licence.
|
mcp_nap-0.2.0/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# mcp-nap
|
|
2
|
+
|
|
3
|
+
Your MCP servers sleep until you call a tool.
|
|
4
|
+
|
|
5
|
+
Claude Code, Cursor and VS Code each start their own copies of every stdio MCP server long before anything calls one, and keep them running: Claude Code one set per session, Cursor as soon as a window opens, VS Code one set per chat. mcp-nap stands in for each server, answers the client's startup requests from a cache, and starts the real server only when one of its tools is called. After 10 quiet minutes it stops it again. The model sees the same tool list either way; the saving is the idle node processes and their memory.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install mcp-nap
|
|
9
|
+
mcp-nap wrap # Claude Code
|
|
10
|
+
mcp-nap wrap --client cursor # Cursor
|
|
11
|
+
mcp-nap wrap --client vscode # VS Code
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`wrap` rewrites every stdio server in that client's config to run through mcp-nap and prints where it saved a backup first. Name servers to wrap only those: `mcp-nap wrap playwright shadcn`. What to do after `wrap` depends on the client, below. Servers that hold state lose it at the 10-minute stop: Playwright's open browser is the usual one, see [Servers that keep state](#servers-that-keep-state).
|
|
15
|
+
|
|
16
|
+
To try it without installing:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uvx mcp-nap wrap
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That works, but uvx stays running as the parent of every wrapped server, so each one costs about 56 MB instead of 22 MB. With four servers that is 223 MB per session instead of 89 MB. It also writes `uvx mcp-nap` into the config with no version, so which release runs is up to uv's cache at each start. Install it with `uv tool install` (or `pipx install mcp-nap`) once you keep it.
|
|
23
|
+
|
|
24
|
+
## Let your agent set it up
|
|
25
|
+
|
|
26
|
+
Paste this to Claude Code, Cursor, VS Code chat or any agent that can run commands on your machine:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Set up mcp-nap (https://github.com/ashuwhy/mcp-nap) so my stdio MCP servers sleep until a tool is called.
|
|
30
|
+
|
|
31
|
+
1. If `uv --version` fails, install uv the way https://docs.astral.sh/uv/getting-started/installation/ says. Run `uv tool install mcp-nap`, then `mcp-nap --help`. If mcp-nap is not found, run `uv tool update-shell` and use a new shell.
|
|
32
|
+
2. Check which of these I use and list the stdio servers (entries with a "command") in each. Read the files only; never print env values or headers, they often hold API keys.
|
|
33
|
+
- Claude Code: `claude` is on PATH; top-level "mcpServers" in ~/.claude.json
|
|
34
|
+
- Cursor: "mcpServers" in ~/.cursor/mcp.json
|
|
35
|
+
- VS Code: "servers" in ~/Library/Application Support/Code/User/mcp.json (macOS), %APPDATA%\Code\User\mcp.json (Windows) or ~/.config/Code/User/mcp.json (Linux)
|
|
36
|
+
3. Show me the list and ask which to wrap. Flag servers that keep state between calls (Playwright and other browser servers) and suggest --idle 0 for them.
|
|
37
|
+
4. Wrap with mcp-nap only, never by editing a config yourself:
|
|
38
|
+
Claude Code: mcp-nap wrap NAME...
|
|
39
|
+
Cursor: mcp-nap wrap NAME... --client cursor
|
|
40
|
+
VS Code: mcp-nap wrap NAME... --client vscode
|
|
41
|
+
Wrap the stateful ones in a separate run with --idle 0 added.
|
|
42
|
+
5. Tell me what each run printed (backup path, any note, the restart step), and that `mcp-nap unwrap` with the same --client undoes it.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Clients
|
|
46
|
+
|
|
47
|
+
Checked against Claude Code 2.1, Cursor 3.29 and VS Code 1.146 on macOS. All three change how they run servers often.
|
|
48
|
+
|
|
49
|
+
### Claude Code
|
|
50
|
+
|
|
51
|
+
Every session starts every stdio server and keeps it until the session ends, so five open sessions hold five idle copies of each. `mcp-nap wrap` edits the user servers in `~/.claude.json` (or `$CLAUDE_CONFIG_DIR/.claude.json`) through `claude mcp`, because live sessions write that file too. Then:
|
|
52
|
+
|
|
53
|
+
> Restart open Claude Code sessions to pick this up.
|
|
54
|
+
|
|
55
|
+
### Cursor
|
|
56
|
+
|
|
57
|
+
As soon as a signed-in window opens, Cursor starts every enabled server, before any chat, and keeps it until you quit. A second window or workspace can add more copies. `mcp-nap wrap --client cursor` edits `~/.cursor/mcp.json` (`%USERPROFILE%\.cursor\mcp.json` on Windows). Then:
|
|
58
|
+
|
|
59
|
+
> Cursor starts the new command by itself, but copies already running keep the old one: toggle each changed server off and on in Cursor Settings > Tools & MCP, or quit and reopen Cursor.
|
|
60
|
+
|
|
61
|
+
Project `.cursor/mcp.json` files are not touched.
|
|
62
|
+
|
|
63
|
+
### VS Code
|
|
64
|
+
|
|
65
|
+
Chat in VS Code 1.146 runs on the Copilot agent host, which starts a full set of servers for every chat session and keeps it until you delete the chat or quit. Archiving a chat doesn't stop its set. Opening an empty new chat starts one more, dropped after 5 minutes if you never type. Wrapped, every one of those copies is a sleeping 22 MB wrapper, and the real server runs only around a tool call.
|
|
66
|
+
|
|
67
|
+
`mcp-nap wrap --client vscode` edits the default profile's `mcp.json`: `~/Library/Application Support/Code/User/mcp.json` on macOS, `%APPDATA%\Code\User\mcp.json` on Windows, `~/.config/Code/User/mcp.json` on Linux (or under `$XDG_CONFIG_HOME`). Then:
|
|
68
|
+
|
|
69
|
+
> VS Code picks up the new command in each chat the next time you send a message in it, or quit and reopen VS Code.
|
|
70
|
+
|
|
71
|
+
- A server with `"sandboxEnabled": true` is left alone: the sandbox would not let mcp-nap write its cache.
|
|
72
|
+
- On the older Local harness (`chat.defaultToCopilotHarness` off), VS Code already starts servers on first use, so mcp-nap only adds the idle stop. That harness also needs `rg` (ripgrep) on PATH to start any sandboxed server, wrapped or not; without it the server exits with `ripgrep (rg) not found`.
|
|
73
|
+
- Other profiles, VS Code Insiders and workspace `.vscode/mcp.json` files are not touched.
|
|
74
|
+
|
|
75
|
+
### Cursor and VS Code config files
|
|
76
|
+
|
|
77
|
+
`wrap` and `unwrap` write the file the way it was laid out (indent, line endings, key order), so `unwrap` gives back the same bytes. A file laid out by hand gets reformatted once, with a note that the backup has it as it was. A file with comments or trailing commas is refused and left as it is, since those would be lost.
|
|
78
|
+
|
|
79
|
+
### Any other client
|
|
80
|
+
|
|
81
|
+
mcp-nap is a plain stdio server, so any client that runs stdio servers can use it. Put it in front of the server's command:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
"playwright": {
|
|
85
|
+
"command": "mcp-nap",
|
|
86
|
+
"args": ["playwright", "--", "npx", "-y", "@playwright/mcp@latest"]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The first argument names the cache file, so give each server its own. If the client doesn't see your shell's PATH, use the full path that `command -v mcp-nap` prints. (`wrap` already writes the full path.)
|
|
91
|
+
|
|
92
|
+
## What it saves
|
|
93
|
+
|
|
94
|
+
With four servers, one session held 948 MB bare and 89 MB wrapped.
|
|
95
|
+
|
|
96
|
+
Measured on my laptop with four npx servers (mui, shadcn, playwright, chrome-devtools) per session. A session here is what Claude Code does at startup: spawn every server, initialize, list tools, then sit idle. In Cursor and VS Code, count one session per set of copies the client starts.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
machine: Darwin arm64, 16 GB RAM, 6026 MB free at start, Python 3.15.2, node v22.16.0
|
|
100
|
+
uv 0.10.2, uv tool install picked Python 3.13.5
|
|
101
|
+
env: npm_config_prefer_offline=true npm_config_update_notifier=false CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1 CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS=1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Resident memory, 4 servers per session, wrapper cache warm:
|
|
105
|
+
|
|
106
|
+
| sessions | bare | mcp-nap (uv tool install) | mcp-nap (python3 mcp_nap.py) | mcp-nap (uvx) |
|
|
107
|
+
|---|---|---|---|---|
|
|
108
|
+
| 1 | 948 MB (948 MB/session) | 89 MB (89 MB/session) | 75 MB (75 MB/session) | 222 MB (222 MB/session) |
|
|
109
|
+
| 3 | 2635 MB (878 MB/session) | 268 MB (89 MB/session) | 228 MB (76 MB/session) | 668 MB (223 MB/session) |
|
|
110
|
+
| 5 | 3836 MB (767 MB/session) | 447 MB (89 MB/session) | 379 MB (76 MB/session) | 1115 MB (223 MB/session) |
|
|
111
|
+
|
|
112
|
+
- mcp-nap (uv tool install) wrapper: 22.4 MB per wrapped server
|
|
113
|
+
- mcp-nap (python3 mcp_nap.py) wrapper: 19.0 MB per wrapped server
|
|
114
|
+
- mcp-nap (uvx) wrapper: 55.7 MB per wrapped server, uvx stayed as parent in 20 of 20
|
|
115
|
+
- memory pressure at 3 sessions, bare: the compressor grew 486 MB while measuring, so that cell reads up to that much low
|
|
116
|
+
- memory pressure at 5 sessions, bare: the compressor grew 826 MB while measuring, so that cell reads up to that much low
|
|
117
|
+
|
|
118
|
+
Delay, median of 3 runs (nap = python3 mcp_nap.py; cold = no mcp-nap cache yet, asleep = warm cache, server not started):
|
|
119
|
+
|
|
120
|
+
| server | bare start | nap start cold | nap start warm | uv tool start warm | uvx start warm | bare call | nap call asleep |
|
|
121
|
+
|---|---|---|---|---|---|---|---|
|
|
122
|
+
| mui | 515 ms | 517 ms | 34 ms | 33 ms | 73 ms | 6 ms | 541 ms |
|
|
123
|
+
| shadcn | 536 ms | 531 ms | 33 ms | 28 ms | 69 ms | 8 ms | 504 ms |
|
|
124
|
+
| playwright | 523 ms | 532 ms | 34 ms | 29 ms | 72 ms | 927 ms | 1.1 s |
|
|
125
|
+
| chrome-devtools | 593 ms | 600 ms | 33 ms | 28 ms | 68 ms | 12 ms | 568 ms |
|
|
126
|
+
|
|
127
|
+
The bare cells at 3 and 5 sessions were measured while macOS was compressing pages, so they read low and the saving there is larger than shown. The 1-session row had no pressure.
|
|
128
|
+
|
|
129
|
+
The delay is paid on the first call to a server after it went to sleep, then calls run at the bare speed until it idles out. Startup gets faster, because the tool list comes from the cache instead of a fresh node process.
|
|
130
|
+
|
|
131
|
+
To rerun it on your machine (needs uv and node, about 5 minutes):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python3 bench/measure.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
It builds the wheel from the checkout, installs it with `uv tool install` into a temp dir, uses its own temp cache, and skips a session count when there isn't free memory for it. `--sessions 1,3` and `--reps 1` make it shorter; `--skip-uv` runs only bare and `python3 mcp_nap.py`.
|
|
138
|
+
|
|
139
|
+
RSS counts shared pages (the node binary, libc) once per process, so every column reads high. Read the difference between columns, not the absolute numbers.
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
|
|
143
|
+
The first time a wrapped server runs, mcp-nap starts it, passes everything through, and saves its `initialize`, `tools/list`, resource and prompt list replies under `~/.cache/mcp-nap` (`$XDG_CACHE_HOME` if set). After that, a new session gets those replies from the cache and the server stays off. Resource subscriptions and the log level, which Cursor and VS Code send on connect, are taken by mcp-nap and handed to the server when it starts. The first other request, usually a tool call, starts it. It stops again after 10 minutes with nothing in flight.
|
|
144
|
+
|
|
145
|
+
If the server's command or its entry script changes, the cache is refilled on the next start. If the lists change anyway, mcp-nap sends `list_changed` and the client refetches.
|
|
146
|
+
|
|
147
|
+
## What sleeping costs
|
|
148
|
+
|
|
149
|
+
- The first call after a stop waits for the server to start, about half a second for the npx servers above.
|
|
150
|
+
- No completions while asleep: `completion/complete` gets an empty list until something wakes the server.
|
|
151
|
+
- No resource updates while asleep. Subscriptions are kept and passed on at the next start, but nothing is running to notice a change in between.
|
|
152
|
+
- State the server holds dies at the stop, below.
|
|
153
|
+
|
|
154
|
+
## Servers that keep state
|
|
155
|
+
|
|
156
|
+
A server that holds state between calls loses it when it stops. Playwright's open browser is the common one. Keep those running for the life of the session with `--idle 0`:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
mcp-nap unwrap playwright && mcp-nap wrap playwright --idle 0
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`--idle N` sets any other timeout in seconds.
|
|
163
|
+
|
|
164
|
+
## Servers whose login expires
|
|
165
|
+
|
|
166
|
+
Some servers keep an OAuth token that dies on a schedule, and every call then fails with `invalid_grant` until you run the server's own login command. `--reauth ARG` does that for you: on such a failure it runs `<command> ARG`, opens the first https URL it prints, and restarts the server once the login exits cleanly. `wrap` doesn't set it, so add it by hand:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
"google-docs": {
|
|
170
|
+
"command": "mcp-nap",
|
|
171
|
+
"args": ["google-docs", "--reauth", "auth", "--", "google-docs-mcp"]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Undo
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
mcp-nap unwrap # or: unwrap --client cursor, unwrap --client vscode
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
That puts every wrapped server back as it was. Env edits you made to a wrapped entry after `wrap` are kept. Or quit the client and copy the backup back over the config. `wrap` printed its path; it sits next to the config as `.claude.json.mcp-nap-<date>-<time>` or `mcp.json.mcp-nap-<date>-<time>`.
|
|
182
|
+
|
|
183
|
+
## http and sse servers
|
|
184
|
+
|
|
185
|
+
They are left alone in every client. For Claude Code, mcp-nap edits the config through `claude mcp remove` and `claude mcp add-json`, because live sessions write `~/.claude.json` too, and removing an http or sse server would also delete its OAuth login. Re-adding a server moves it to the end of the list, so if an http server sits between stdio servers, the stdio servers after it end up after it in the file. `wrap` prints a note when that happens. The order doesn't change how Claude Code runs them.
|
|
186
|
+
|
|
187
|
+
## Limits
|
|
188
|
+
|
|
189
|
+
- macOS, Linux and Windows. The tables above are from macOS. On Linux (a Debian container, node 22, Python 3.11) the tests pass and one `bench/measure.py --sessions 1 --reps 1` run gave 841 MB bare against 42 MB with `uv tool install`.
|
|
190
|
+
- On Windows 11 (arm64, Python 3.9 and 3.13, node 22) the tests pass, and a wrapped `npx` filesystem server starts on the first tool call and leaves no `cmd.exe` or `node.exe` behind after the idle stop, after the session closes, or after the wrapper is killed. Not yet tried there: `wrap` against a real Claude Code install, and Claude Code starting the `mcp-nap` console script. If `claude` is npm's `claude.cmd`, cmd.exe may expand a `%NAME%` inside an env value as `wrap` passes it through.
|
|
191
|
+
- `--client cursor` and `--client vscode` were run against the real apps on macOS only. Their tests pass on Linux and Windows too.
|
|
192
|
+
- Stdio servers only.
|
|
193
|
+
- Without `--client`, `wrap` needs the `claude` CLI on PATH.
|
|
194
|
+
- Without `--client`, `wrap` and `unwrap` pass each server's entry, env values included, to `claude mcp add-json` as a command-line argument. For about a second per server, another user on the same machine can read it in `ps`.
|
|
195
|
+
|
|
196
|
+
MIT licence.
|