mcpmon 0.1.0

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.
@@ -0,0 +1,50 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish-npm:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '20'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Publish to npm
22
+ run: npm publish --access public
23
+
24
+ build-binaries:
25
+ runs-on: ${{ matrix.os }}
26
+ permissions:
27
+ contents: write
28
+ strategy:
29
+ matrix:
30
+ include:
31
+ - os: ubuntu-latest
32
+ target: linux-x64
33
+ - os: macos-latest
34
+ target: darwin-arm64
35
+ - os: macos-13
36
+ target: darwin-x64
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - uses: oven-sh/setup-bun@v2
41
+ with:
42
+ bun-version: latest
43
+
44
+ - name: Build binary
45
+ run: bun build --compile mcpmon.ts --outfile mcpmon-${{ matrix.target }}
46
+
47
+ - name: Upload to release
48
+ run: gh release upload ${{ github.ref_name }} mcpmon-${{ matrix.target }} --clobber
49
+ env:
50
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,26 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+
19
+ - name: Install build
20
+ run: pip install build
21
+
22
+ - name: Build package
23
+ run: python -m build
24
+
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,61 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ bump:
7
+ description: 'Version bump type'
8
+ required: true
9
+ default: 'patch'
10
+ type: choice
11
+ options:
12
+ - patch
13
+ - minor
14
+ - major
15
+
16
+ jobs:
17
+ release:
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ contents: write
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Get current version
31
+ id: current
32
+ run: |
33
+ VERSION=$(grep -Po '(?<=version = ")[^"]+' pyproject.toml)
34
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
35
+
36
+ - name: Bump version
37
+ id: bump
38
+ run: |
39
+ IFS='.' read -r major minor patch <<< "${{ steps.current.outputs.version }}"
40
+ case "${{ inputs.bump }}" in
41
+ major) major=$((major + 1)); minor=0; patch=0 ;;
42
+ minor) minor=$((minor + 1)); patch=0 ;;
43
+ patch) patch=$((patch + 1)) ;;
44
+ esac
45
+ NEW_VERSION="$major.$minor.$patch"
46
+ echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
47
+ sed -i "s/version = \"${{ steps.current.outputs.version }}\"/version = \"$NEW_VERSION\"/" pyproject.toml
48
+
49
+ - name: Commit and tag
50
+ run: |
51
+ git config user.name "github-actions[bot]"
52
+ git config user.email "github-actions[bot]@users.noreply.github.com"
53
+ git add pyproject.toml
54
+ git commit -m "(release): v${{ steps.bump.outputs.version }}"
55
+ git tag "v${{ steps.bump.outputs.version }}"
56
+ git push && git push --tags
57
+
58
+ - name: Create release
59
+ run: gh release create "v${{ steps.bump.outputs.version }}" --generate-notes
60
+ env:
61
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # mcpmon
2
+
3
+ Hot reload for MCP servers. Like nodemon, but for MCP.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Bun (recommended)
9
+ bunx mcpmon
10
+
11
+ # Or install globally
12
+ bun install -g mcpmon
13
+
14
+ # Python alternative
15
+ pip install mcpmon
16
+
17
+ # Or download binary from GitHub releases (no dependencies)
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ mcpmon --watch src/ -- python -m my_mcp_server
24
+ ```
25
+
26
+ ### Options
27
+
28
+ - `--watch, -w` - Directory to watch (default: current directory)
29
+ - `--ext, -e` - File extensions to watch, comma-separated (default: py)
30
+
31
+ ### Examples
32
+
33
+ ```bash
34
+ # Watch current directory for .py changes
35
+ mcpmon -- python server.py
36
+
37
+ # Watch src/ for .py and .json changes
38
+ mcpmon --watch src/ --ext py,json -- python -m myserver
39
+
40
+ # With crucible-mcp
41
+ mcpmon --watch src/crucible/ -- crucible-mcp
42
+
43
+ # With sage-mcp
44
+ mcpmon --watch ~/.sage/ --ext py,yaml -- sage-mcp
45
+ ```
46
+
47
+ ## MCP Config
48
+
49
+ Use mcpmon in your `.mcp.json` for hot reload during development:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "my-server": {
55
+ "command": "mcpmon",
56
+ "args": ["--watch", "src/", "--", "python", "-m", "my_server"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## How it works
63
+
64
+ 1. Starts your MCP server as a subprocess
65
+ 2. Watches specified directory for file changes
66
+ 3. On change: SIGTERM → wait 2s → SIGKILL → restart
67
+ 4. Claude Code automatically reconnects to the restarted server
package/mcpmon.py ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """mcpmon: Hot reload for MCP servers. Like nodemon, but for MCP."""
3
+
4
+ import argparse
5
+ import subprocess
6
+ import signal
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from watchfiles import watch, Change
11
+
12
+
13
+ def parse_args():
14
+ parser = argparse.ArgumentParser(
15
+ description="Hot reload wrapper for MCP servers",
16
+ usage="mcpmon --watch <dir> [--ext <ext>] -- <command>",
17
+ )
18
+ parser.add_argument(
19
+ "--watch", "-w",
20
+ type=str,
21
+ default=".",
22
+ help="Directory to watch (default: current directory)",
23
+ )
24
+ parser.add_argument(
25
+ "--ext", "-e",
26
+ type=str,
27
+ default="py",
28
+ help="File extensions to watch, comma-separated (default: py)",
29
+ )
30
+ parser.add_argument(
31
+ "command",
32
+ nargs=argparse.REMAINDER,
33
+ help="Command to run (after --)",
34
+ )
35
+ args = parser.parse_args()
36
+
37
+ # Remove leading -- from command if present
38
+ if args.command and args.command[0] == "--":
39
+ args.command = args.command[1:]
40
+
41
+ if not args.command:
42
+ parser.error("No command specified. Use: mcpmon --watch src/ -- <command>")
43
+
44
+ return args
45
+
46
+
47
+ def terminate_process(proc: subprocess.Popen) -> None:
48
+ """Gracefully terminate process: SIGTERM, wait 2s, SIGKILL."""
49
+ if proc.poll() is not None:
50
+ return
51
+
52
+ proc.terminate()
53
+ try:
54
+ proc.wait(timeout=2)
55
+ except subprocess.TimeoutExpired:
56
+ proc.kill()
57
+ proc.wait()
58
+
59
+
60
+ def start_process(command: list[str]) -> subprocess.Popen:
61
+ """Start the MCP server process."""
62
+ return subprocess.Popen(command)
63
+
64
+
65
+ def should_reload(changes: set, extensions: set[str]) -> bool:
66
+ """Check if any changed file matches our extensions."""
67
+ for change_type, path in changes:
68
+ if change_type in (Change.added, Change.modified):
69
+ if Path(path).suffix.lstrip(".") in extensions:
70
+ return True
71
+ return False
72
+
73
+
74
+ def main():
75
+ args = parse_args()
76
+
77
+ watch_path = Path(args.watch).resolve()
78
+ extensions = {ext.strip().lstrip(".") for ext in args.ext.split(",")}
79
+ command = args.command
80
+
81
+ print(f"[mcpmon] Watching {watch_path} for .{', .'.join(extensions)} changes")
82
+ print(f"[mcpmon] Running: {' '.join(command)}")
83
+
84
+ proc = start_process(command)
85
+
86
+ # Handle Ctrl+C gracefully
87
+ def signal_handler(signum, frame):
88
+ print("\n[mcpmon] Shutting down...")
89
+ terminate_process(proc)
90
+ sys.exit(0)
91
+
92
+ signal.signal(signal.SIGINT, signal_handler)
93
+ signal.signal(signal.SIGTERM, signal_handler)
94
+
95
+ try:
96
+ for changes in watch(watch_path):
97
+ if should_reload(changes, extensions):
98
+ changed_files = [p for _, p in changes]
99
+ print(f"[mcpmon] Change detected: {', '.join(changed_files)}")
100
+ print("[mcpmon] Restarting...")
101
+
102
+ terminate_process(proc)
103
+ proc = start_process(command)
104
+
105
+ print("[mcpmon] Server restarted")
106
+ except KeyboardInterrupt:
107
+ pass
108
+ finally:
109
+ terminate_process(proc)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
package/mcpmon.ts ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * mcpmon: Hot reload for MCP servers. Like nodemon, but for MCP.
4
+ */
5
+
6
+ import { watch } from "fs";
7
+ import { spawn, type Subprocess } from "bun";
8
+ import { parseArgs } from "util";
9
+
10
+ const { values, positionals } = parseArgs({
11
+ args: Bun.argv.slice(2),
12
+ options: {
13
+ watch: { type: "string", short: "w", default: "." },
14
+ ext: { type: "string", short: "e", default: "py" },
15
+ help: { type: "boolean", short: "h", default: false },
16
+ },
17
+ allowPositionals: true,
18
+ strict: false,
19
+ });
20
+
21
+ if (values.help || positionals.length === 0) {
22
+ console.log(`mcpmon - Hot reload for MCP servers
23
+
24
+ Usage: mcpmon [options] -- <command>
25
+
26
+ Options:
27
+ -w, --watch <dir> Directory to watch (default: .)
28
+ -e, --ext <exts> Extensions to watch, comma-separated (default: py)
29
+ -h, --help Show this help
30
+
31
+ Examples:
32
+ mcpmon -- python server.py
33
+ mcpmon --watch src/ --ext py,json -- python -m myserver
34
+ mcpmon --watch src/crucible/ -- crucible-mcp
35
+ `);
36
+ process.exit(0);
37
+ }
38
+
39
+ // Remove leading "--" if present
40
+ const command = positionals[0] === "--" ? positionals.slice(1) : positionals;
41
+
42
+ if (command.length === 0) {
43
+ console.error("[mcpmon] Error: No command specified");
44
+ process.exit(1);
45
+ }
46
+
47
+ const watchDir = values.watch as string;
48
+ const extensions = new Set((values.ext as string).split(",").map(e => e.trim().replace(/^\./, "")));
49
+
50
+ let proc: Subprocess | null = null;
51
+
52
+ function startServer(): void {
53
+ console.log(`[mcpmon] Starting: ${command.join(" ")}`);
54
+ proc = spawn({
55
+ cmd: command,
56
+ stdout: "inherit",
57
+ stderr: "inherit",
58
+ stdin: "inherit",
59
+ });
60
+ }
61
+
62
+ async function stopServer(): Promise<void> {
63
+ if (!proc || proc.exitCode !== null) return;
64
+
65
+ proc.kill("SIGTERM");
66
+
67
+ // Wait up to 2 seconds for graceful shutdown
68
+ const timeout = setTimeout(() => {
69
+ if (proc && proc.exitCode === null) {
70
+ proc.kill("SIGKILL");
71
+ }
72
+ }, 2000);
73
+
74
+ await proc.exited;
75
+ clearTimeout(timeout);
76
+ }
77
+
78
+ async function restartServer(): Promise<void> {
79
+ await stopServer();
80
+ startServer();
81
+ }
82
+
83
+ function shouldReload(filename: string | null): boolean {
84
+ if (!filename) return false;
85
+ const ext = filename.split(".").pop() || "";
86
+ return extensions.has(ext);
87
+ }
88
+
89
+ // Start server
90
+ console.log(`[mcpmon] Watching ${watchDir} for .${[...extensions].join(", .")} changes`);
91
+ startServer();
92
+
93
+ // Watch for changes
94
+ const watcher = watch(watchDir, { recursive: true }, async (event, filename) => {
95
+ if (event === "change" && shouldReload(filename)) {
96
+ console.log(`[mcpmon] ${filename} changed, restarting...`);
97
+ await restartServer();
98
+ }
99
+ });
100
+
101
+ // Handle shutdown
102
+ process.on("SIGINT", async () => {
103
+ console.log("\n[mcpmon] Shutting down...");
104
+ watcher.close();
105
+ await stopServer();
106
+ process.exit(0);
107
+ });
108
+
109
+ process.on("SIGTERM", async () => {
110
+ watcher.close();
111
+ await stopServer();
112
+ process.exit(0);
113
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "mcpmon",
3
+ "version": "0.1.0",
4
+ "description": "Hot reload for MCP servers. Like nodemon, but for MCP.",
5
+ "type": "module",
6
+ "main": "mcpmon.ts",
7
+ "bin": {
8
+ "mcpmon": "mcpmon.ts"
9
+ },
10
+ "scripts": {
11
+ "build": "bun build --compile mcpmon.ts --outfile dist/mcpmon"
12
+ },
13
+ "keywords": ["mcp", "hot-reload", "nodemon", "bun", "development"],
14
+ "author": "be.nvy",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/b17z/mcpmon"
19
+ },
20
+ "engines": {
21
+ "bun": ">=1.0.0"
22
+ }
23
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcpmon"
7
+ version = "0.1.0"
8
+ description = "Hot reload for MCP servers. Like nodemon, but for MCP."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "be.nvy" }]
13
+ keywords = ["mcp", "hot-reload", "development", "nodemon"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+ dependencies = [
25
+ "watchfiles>=0.20.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ mcpmon = "mcpmon:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/b17z/mcpmon"