Guide · Developer Experience

MCP server hot reload

The default MCP development loop — edit TypeScript, run tsc, restart the process, reconnect the MCP Inspector, clear conversation context, re-run the tool — takes 15–30 seconds per iteration. At 20 iterations per hour, that is 5–10 minutes of waiting. tsx --watch cuts this to under 2 seconds: it restarts the MCP server process in under a second after any file change, and the MCP Inspector reconnects automatically over stdio. The result is a tight feedback loop where you can iterate on tool logic, error messages, and output formatting as fast as you can type.

TL;DR

Install tsx and add "dev": "tsx --watch src/index.ts" to package.json scripts. Run npm run dev. Open the MCP Inspector pointed at the same command — it reconnects on every restart. Edit your tool handlers; changes are live in under 2 seconds. For nodemon (JavaScript or compiled output), use nodemon --watch src --ext ts,js --exec 'node dist/index.js' with a tsc --watch in a second terminal. Do not use module-level singletons for stateful resources (DB connections, HTTP clients) — initialize them in a factory function so a fresh connection is created on each restart.

Why hot reload matters for MCP development

MCP servers are tested interactively: you write a tool handler, call it from the Inspector or Claude Desktop, see the output, and adjust. The iteration speed depends entirely on how fast you can get from "saved file" to "tool call returned updated result." With tsc compilation, a cold restart takes 8–20 seconds for a typical project. With tsx --watch, it takes 0.5–1.5 seconds.

The other advantage of a fast restart is that it reveals stateful bugs early. If your server accumulates state during development (a module-level array, a database connection that fails to reconnect, a cache that isn't cleared), a slow restart tempts you to keep the same process running across many edits. Fast restart makes process isolation the default, which matches production behavior.

tsx --watch — the recommended setup

tsx is a TypeScript executor built on esbuild. Unlike ts-node, it strips types rather than type-checking, so it starts in milliseconds. --watch restarts the process on any .ts, .tsx, .js, or .json change in the project.

npm install --save-dev tsx

Add the dev script to package.json:

{
  "scripts": {
    "dev":   "tsx --watch src/index.ts",
    "build": "tsc --noEmit && tsc --outDir dist",
    "start": "node dist/index.js"
  }
}

Run npm run dev. The MCP server starts on stdio. In MCP Inspector, configure it to run npm run dev as the server command — it keeps the process connected and reconnects automatically after each restart.

ToolRestart timeType checkingBest for
tsx --watch0.5–1.5 sNone (strips types)Daily development iteration
ts-node-dev2–5 sPartial (transpile only)Legacy projects already using ts-node
tsc --watch + nodemon4–8 sFull type check on saveWhen type errors must not be silently skipped
Manual tsc + restart10–25 sFull type checkCI / production builds only

The tradeoff with tsx: type errors are invisible during development. Run tsc --noEmit in a separate terminal or as a pre-commit hook to catch type errors before pushing. Do not skip type checking entirely — it catches real bugs, especially in tool handler dispatch code where the wrong type can cause a silent wrong-tool call.

Configuring MCP Inspector for auto-reconnect

The MCP Inspector connects to a server over stdio by spawning a subprocess. When the subprocess exits (on reload), the Inspector detects the exit and shows a "Disconnected" state. To reconnect automatically:

  1. In Inspector → Settings → Server, set Command to npm and Args to run dev (not the path to the compiled file). When the watcher restarts the server process, Inspector respawns its subprocess.
  2. Alternatively, use Inspector's --watch flag: npx @modelcontextprotocol/inspector --watch npm run dev — Inspector polls for reconnection every 500ms after a disconnect.
  3. For SSE/HTTP MCP servers (not stdio), the Inspector retries the HTTP connection automatically — set Reconnect delay to 1000ms in Inspector settings.
# Terminal 1 — MCP server in watch mode
npm run dev

# Terminal 2 — Inspector (connects to the above)
npx @modelcontextprotocol/inspector npm run dev

# Or as one command (Inspector manages the server subprocess):
npx @modelcontextprotocol/inspector --command "npm run dev"

Hot reload with Claude Desktop

Claude Desktop reads the MCP server config from ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows). It spawns the server process once at startup and does not automatically reconnect on process exit — a full Claude Desktop restart is required.

Two workarounds during development:

  1. Use Inspector as primary dev client, Claude Desktop for final validation. Iterate in Inspector, then do a final test in Claude Desktop before each meaningful commit.
  2. Wrap the server in a persistent process manager. Configure Claude Desktop to run tsx --watch src/index.ts as the server command. Claude Desktop will restart the process on exit — but note it may take 2–5 seconds for Claude Desktop to detect the exit and restart, which is slightly slower than Inspector's reconnect.
// claude_desktop_config.json — dev config
{
  "mcpServers": {
    "my-server-dev": {
      "command": "npx",
      "args": ["tsx", "--watch", "/path/to/your/server/src/index.ts"],
      "env": {
        "NODE_ENV": "development",
        "API_BASE_URL": "http://localhost:3000"
      }
    }
  }
}

Keep a separate "my-server-prod" entry pointing at the compiled dist/index.js so you can switch between dev and production without editing the config file.

Server structure for reliable hot reload

Hot reload restarts the Node.js process — module-level singletons are re-initialized on each restart. This is almost always what you want, but two patterns cause problems:

  1. Module-level database connections: if you open a SQLite or Postgres connection at module load time and the connection state is not properly closed on process exit, restarting during a write can corrupt the database file. Use a factory function (createDeps()) that opens connections after the Server is constructed, and wire a process.on('SIGTERM') / process.on('SIGINT') handler to close them cleanly.
  2. In-memory state shared across tool calls: if tools share a module-level cache or accumulator for test purposes, each reload starts fresh. This is correct for production but can be frustrating during development if you are testing incremental behavior. Use an external state store (SQLite file, Redis) that survives restarts, or accept the clean-slate behavior.
// src/server.ts — structured for clean restart
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createDeps } from './deps.js';
import { registerTools } from './tools.js';

async function main() {
  const deps = await createDeps(); // opens DB connection, HTTP client, etc.

  const server = new Server(
    { name: 'my-server', version: '1.0.0' },
    { capabilities: { tools: {} } }
  );

  registerTools(server, deps);

  const transport = new StdioServerTransport();
  await server.connect(transport);

  // Clean shutdown on SIGTERM (tsx --watch sends this before restarting)
  const shutdown = async () => { await deps.db.close(); process.exit(0); };
  process.on('SIGTERM', shutdown);
  process.on('SIGINT',  shutdown);
}

main().catch(console.error);

tsx --watch sends SIGTERM to the old process before starting the new one. If your server handles SIGTERM and closes connections cleanly, the restart is safe. If it does not, and you are writing to a SQLite file, add journal_mode=WAL in your PRAGMA setup — WAL mode is resilient to abrupt process termination.

Watching non-TypeScript files

tsx --watch watches .ts, .tsx, .js, and .json files by default. To also restart on changes to .sql migration files, .yaml config files, or .graphql schemas, use --watch with an explicit pattern:

# Watch TypeScript source and YAML config files
tsx --watch --watch-path src --watch-path config src/index.ts

# Or use nodemon for more glob control:
nodemon --watch src --watch config --ext ts,js,yaml --exec 'tsx src/index.ts'

For OpenAPI spec files: add --watch-path openapi.yaml so the server restarts when the spec changes — essential if you are doing runtime dynamic tool generation from the spec (see OpenAPI to MCP).

Debugging with hot reload

Node.js's --inspect flag opens a debugging port. Combine it with tsx --watch to get hot-reloading and a debugger:

# In package.json scripts:
"dev:debug": "tsx --watch --inspect src/index.ts"

# Attach in VS Code with .vscode/launch.json:
{
  "configurations": [{
    "name": "Attach to tsx --watch",
    "type": "node",
    "request": "attach",
    "port": 9229,
    "restart": true,    // re-attaches after each reload
    "sourceMaps": true,
    "outFiles": ["${workspaceFolder}/src/**/*.ts"]
  }]
}

"restart": true in the VS Code launch config makes the debugger automatically reattach after each hot reload. Combined with tsx --watch restarting in under a second, you get a live debugger that follows your server through every code change.

What hot reload does not replace

Hot reload speeds up the inner development loop. It does not replace:

Related questions

Does tsx --watch work with ES modules (type: "module")?

Yes. tsx handles both CommonJS and ES module projects. If your package.json has "type": "module", tsx runs your files as ESM automatically. No change to the --watch command is needed. One caveat: dynamic require() calls don't work in ESM mode — use import() for dynamic imports.

Is there a way to hot-reload without full process restart?

Node.js's experimental --watch flag (added in Node 18) restarts the process on file change, similar to tsx --watch. It does not support module-level hot patching (replacing individual module exports without restarting). For MCP servers, full process restart is the right model — the MCP protocol is stateless at the tool level, and a clean restart ensures the server state always matches the code. Module-level hot patching (like Vite's HMR) is useful for frontend UI state but adds complexity without benefit in the MCP server context.

Why is my server slow to restart even with tsx --watch?

Common causes: (1) The server imports a large module at startup (e.g., a 5MB OpenAPI spec or a slow database connection). Move expensive initialization after the server is constructed, not at module load. (2) The createDeps() factory awaits a database migration on every start — skip migrations in development mode with if (process.env.NODE_ENV !== 'development') await runMigrations(). (3) tsx is watching too many files — add a .watchignore to exclude node_modules, dist, and .git.

How do I prevent hot reload from breaking my test database?

Use a separate SQLite file for development: DB_PATH=./data/dev.db npm run dev. Hot reload clears in-memory state but not the file. If you want each restart to start with a clean database, add a --reset-db flag: check process.argv.includes('--reset-db') in createDeps() and delete + recreate the dev DB file. Never reset automatically on every startup — you'll lose data from the previous iteration that you need to inspect.

Further reading