mcp-twin 1.2.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,247 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example MCP Server with HTTP Mode Support for MCP Twin
4
+
5
+ This template shows how to add HTTP mode to your MCP server for zero-downtime reloads.
6
+
7
+ Usage:
8
+ # Stdio mode (default, for Claude Code direct connection)
9
+ python3 http-server.py
10
+
11
+ # HTTP mode (for MCP Twin)
12
+ python3 http-server.py --http --port 8101
13
+ """
14
+
15
+ import sys
16
+ import json
17
+ from http.server import HTTPServer, BaseHTTPRequestHandler
18
+
19
+
20
+ # =============================================================================
21
+ # YOUR MCP TOOLS (Replace with your actual tools)
22
+ # =============================================================================
23
+
24
+ def my_tool(arg1: str, arg2: int = 10) -> dict:
25
+ """Example tool - replace with your actual implementation."""
26
+ return {
27
+ "ok": True,
28
+ "result": f"Processed {arg1} with value {arg2}"
29
+ }
30
+
31
+
32
+ TOOLS = [
33
+ {
34
+ "name": "my_tool",
35
+ "description": "An example tool that processes input",
36
+ "inputSchema": {
37
+ "type": "object",
38
+ "properties": {
39
+ "arg1": {"type": "string", "description": "First argument"},
40
+ "arg2": {"type": "integer", "description": "Second argument", "default": 10}
41
+ },
42
+ "required": ["arg1"]
43
+ }
44
+ }
45
+ ]
46
+
47
+
48
+ # =============================================================================
49
+ # MCP PROTOCOL HANDLERS
50
+ # =============================================================================
51
+
52
+ def send_response(response: dict):
53
+ """Send JSON-RPC response to stdout."""
54
+ output = json.dumps(response)
55
+ sys.stdout.write(output + "\n")
56
+ sys.stdout.flush()
57
+
58
+
59
+ def send_error(request_id, code: int, message: str):
60
+ """Send JSON-RPC error response."""
61
+ send_response({
62
+ "jsonrpc": "2.0",
63
+ "id": request_id,
64
+ "error": {"code": code, "message": message}
65
+ })
66
+
67
+
68
+ def handle_initialize(request_id):
69
+ """Handle MCP initialize request."""
70
+ send_response({
71
+ "jsonrpc": "2.0",
72
+ "id": request_id,
73
+ "result": {
74
+ "protocolVersion": "2024-11-05",
75
+ "capabilities": {"tools": {}},
76
+ "serverInfo": {"name": "my-mcp-server", "version": "1.0.0"}
77
+ }
78
+ })
79
+
80
+
81
+ def handle_list_tools(request_id):
82
+ """Handle tools/list request."""
83
+ send_response({
84
+ "jsonrpc": "2.0",
85
+ "id": request_id,
86
+ "result": {"tools": TOOLS}
87
+ })
88
+
89
+
90
+ def handle_call_tool(request_id, params: dict):
91
+ """Handle tools/call request."""
92
+ tool_name = params.get("name")
93
+ arguments = params.get("arguments", {})
94
+
95
+ try:
96
+ if tool_name == "my_tool":
97
+ result = my_tool(**arguments)
98
+ else:
99
+ send_error(request_id, -32601, f"Unknown tool: {tool_name}")
100
+ return
101
+
102
+ send_response({
103
+ "jsonrpc": "2.0",
104
+ "id": request_id,
105
+ "result": {
106
+ "content": [{"type": "text", "text": json.dumps(result)}]
107
+ }
108
+ })
109
+ except Exception as e:
110
+ send_error(request_id, -32603, str(e))
111
+
112
+
113
+ # =============================================================================
114
+ # STDIO MODE (Default for Claude Code)
115
+ # =============================================================================
116
+
117
+ def main_stdio():
118
+ """Run MCP server in stdio mode."""
119
+ print("[INFO] MCP Server starting (stdio mode)", file=sys.stderr)
120
+
121
+ for line in sys.stdin:
122
+ try:
123
+ request = json.loads(line)
124
+ method = request.get("method")
125
+ request_id = request.get("id")
126
+ params = request.get("params", {})
127
+
128
+ if method == "initialize":
129
+ handle_initialize(request_id)
130
+ elif method == "tools/list":
131
+ handle_list_tools(request_id)
132
+ elif method == "tools/call":
133
+ handle_call_tool(request_id, params)
134
+ else:
135
+ send_error(request_id, -32601, f"Unknown method: {method}")
136
+
137
+ except json.JSONDecodeError:
138
+ send_error(None, -32700, "Parse error")
139
+ except Exception as e:
140
+ print(f"[ERROR] {e}", file=sys.stderr)
141
+ send_error(None, -32603, str(e))
142
+
143
+
144
+ # =============================================================================
145
+ # HTTP MODE (For MCP Twin)
146
+ # =============================================================================
147
+
148
+ def handle_http_request(request_body: str) -> str:
149
+ """Handle HTTP request body and return response."""
150
+ import io
151
+
152
+ try:
153
+ request = json.loads(request_body)
154
+ method = request.get("method")
155
+ request_id = request.get("id")
156
+ params = request.get("params", {})
157
+
158
+ # Capture stdout
159
+ old_stdout = sys.stdout
160
+ sys.stdout = captured = io.StringIO()
161
+
162
+ try:
163
+ if method == "initialize":
164
+ handle_initialize(request_id)
165
+ elif method == "tools/list":
166
+ handle_list_tools(request_id)
167
+ elif method == "tools/call":
168
+ handle_call_tool(request_id, params)
169
+ else:
170
+ send_error(request_id, -32601, f"Unknown method: {method}")
171
+ finally:
172
+ sys.stdout = old_stdout
173
+
174
+ return captured.getvalue().strip()
175
+
176
+ except json.JSONDecodeError:
177
+ return json.dumps({
178
+ "jsonrpc": "2.0",
179
+ "id": None,
180
+ "error": {"code": -32700, "message": "Parse error"}
181
+ })
182
+ except Exception as e:
183
+ return json.dumps({
184
+ "jsonrpc": "2.0",
185
+ "id": None,
186
+ "error": {"code": -32603, "message": str(e)}
187
+ })
188
+
189
+
190
+ def run_http_server(port: int):
191
+ """Run MCP server in HTTP mode for MCP Twin."""
192
+
193
+ class MCPHandler(BaseHTTPRequestHandler):
194
+ def log_message(self, format, *args):
195
+ print(f"[HTTP] {args[0]}", file=sys.stderr)
196
+
197
+ def do_POST(self):
198
+ if self.path == "/mcp":
199
+ content_length = int(self.headers.get('Content-Length', 0))
200
+ body = self.rfile.read(content_length).decode('utf-8')
201
+
202
+ response = handle_http_request(body)
203
+
204
+ self.send_response(200)
205
+ self.send_header('Content-Type', 'application/json')
206
+ self.end_headers()
207
+ self.wfile.write(response.encode('utf-8'))
208
+ else:
209
+ self.send_response(404)
210
+ self.end_headers()
211
+
212
+ def do_GET(self):
213
+ if self.path == "/health":
214
+ self.send_response(200)
215
+ self.send_header('Content-Type', 'application/json')
216
+ self.end_headers()
217
+ self.wfile.write(b'{"status": "ok", "server": "my-mcp-server"}')
218
+ else:
219
+ self.send_response(404)
220
+ self.end_headers()
221
+
222
+ print(f"[INFO] MCP Server starting (HTTP mode) on port {port}", file=sys.stderr)
223
+ server = HTTPServer(('localhost', port), MCPHandler)
224
+
225
+ try:
226
+ server.serve_forever()
227
+ except KeyboardInterrupt:
228
+ print("[INFO] Server stopping", file=sys.stderr)
229
+ server.shutdown()
230
+
231
+
232
+ # =============================================================================
233
+ # MAIN ENTRY POINT
234
+ # =============================================================================
235
+
236
+ if __name__ == "__main__":
237
+ import argparse
238
+
239
+ parser = argparse.ArgumentParser(description="MCP Server with HTTP Mode Support")
240
+ parser.add_argument("--http", action="store_true", help="Run as HTTP server (for MCP Twin)")
241
+ parser.add_argument("--port", type=int, default=8101, help="HTTP port (default: 8101)")
242
+ args = parser.parse_args()
243
+
244
+ if args.http:
245
+ run_http_server(args.port)
246
+ else:
247
+ main_stdio()
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "mcp-twin",
3
+ "displayName": "MCP Twin - Hot Reload",
4
+ "version": "1.2.0",
5
+ "description": "Zero-downtime updates for MCP servers. No more restarting Claude Code when you change server code.",
6
+ "author": {
7
+ "name": "Prax Labs",
8
+ "url": "https://prax.chat"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/nothinginfinity/mcp-twin"
13
+ },
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "mcp",
18
+ "hot-reload",
19
+ "zero-downtime",
20
+ "development",
21
+ "plugin",
22
+ "prax",
23
+ "prax-chat"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "claude-code": ">=1.0.0"
28
+ },
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "bin": {
32
+ "mcp-twin": "./dist/cli.js"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "dev": "tsc --watch",
37
+ "test": "jest"
38
+ },
39
+ "claudePlugin": {
40
+ "type": "skill",
41
+ "commands": [
42
+ {
43
+ "name": "twin",
44
+ "description": "Manage MCP server twins",
45
+ "subcommands": [
46
+ {
47
+ "name": "start",
48
+ "description": "Start twin servers",
49
+ "args": "[server]"
50
+ },
51
+ {
52
+ "name": "stop",
53
+ "description": "Stop twin servers",
54
+ "args": "[server]"
55
+ },
56
+ {
57
+ "name": "reload",
58
+ "description": "Reload standby server",
59
+ "args": "<server>"
60
+ },
61
+ {
62
+ "name": "swap",
63
+ "description": "Switch to standby server",
64
+ "args": "<server>"
65
+ },
66
+ {
67
+ "name": "status",
68
+ "description": "Show twin status",
69
+ "args": "[server]"
70
+ }
71
+ ]
72
+ }
73
+ ],
74
+ "hooks": [
75
+ {
76
+ "event": "file:change",
77
+ "pattern": "**/*_server.py",
78
+ "handler": "onServerFileChange"
79
+ }
80
+ ],
81
+ "permissions": [
82
+ "process:spawn",
83
+ "process:kill",
84
+ "fs:read",
85
+ "fs:write",
86
+ "net:localhost"
87
+ ]
88
+ },
89
+ "devDependencies": {
90
+ "@types/node": "^20.19.27",
91
+ "jest": "^29.0.0",
92
+ "typescript": "^5.0.0"
93
+ },
94
+ "dependencies": {
95
+ "chokidar": "^3.5.3"
96
+ }
97
+ }
package/skills/twin.md ADDED
@@ -0,0 +1,186 @@
1
+ # MCP Twin Skill
2
+
3
+ name: twin
4
+ description: Zero-downtime MCP server management with twin/hot-swap architecture
5
+ version: 1.0.0
6
+
7
+ ## Triggers
8
+ - /twin
9
+ - hot reload mcp
10
+ - reload mcp server
11
+ - swap mcp server
12
+ - mcp twin
13
+
14
+ ## Instructions
15
+
16
+ You manage MCP server twins for zero-downtime development updates.
17
+
18
+ ### Core Concept
19
+ Twin servers = two instances of the same MCP server running on different ports.
20
+ - One is "active" (receiving traffic)
21
+ - One is "standby" (ready to take over)
22
+
23
+ When developer edits code:
24
+ 1. Reload standby with new code
25
+ 2. Verify standby is healthy
26
+ 3. Swap traffic to standby
27
+ 4. Old active becomes new standby
28
+
29
+ Result: Zero downtime, no Claude Code restart needed.
30
+
31
+ ### Commands
32
+
33
+ #### /twin start [server]
34
+ Start twin servers for an MCP server.
35
+
36
+ If no server specified, show detected servers and let user choose.
37
+ Auto-detect from `~/Library/Application Support/Claude/claude_desktop_config.json`
38
+
39
+ Example output:
40
+ ```
41
+ Started twins for inbox-collab
42
+
43
+ Server A: port 8101 (active)
44
+ Server B: port 8102 (standby)
45
+
46
+ Edit your code, then: /twin reload inbox-collab
47
+ ```
48
+
49
+ #### /twin reload <server>
50
+ Reload the standby server with updated code.
51
+
52
+ 1. Stop standby process
53
+ 2. Start new process with same port
54
+ 3. Health check
55
+ 4. Report status
56
+
57
+ Example output:
58
+ ```
59
+ Reloaded inbox-collab standby
60
+
61
+ Standby (B): port 8102
62
+ Health: passing
63
+ Ready to swap
64
+
65
+ Run: /twin swap inbox-collab
66
+ ```
67
+
68
+ If health check fails:
69
+ ```
70
+ Reload failed for inbox-collab
71
+
72
+ Standby (B): port 8102
73
+ Health: FAILED
74
+
75
+ Check logs: ~/.mcp-twin/logs/inbox-collab_b.log
76
+ ```
77
+
78
+ #### /twin swap <server>
79
+ Switch traffic to the standby server.
80
+
81
+ Only swap if standby is healthy. Otherwise warn user.
82
+
83
+ Example output:
84
+ ```
85
+ Swapped inbox-collab
86
+
87
+ Previous active: A (port 8101)
88
+ New active: B (port 8102)
89
+
90
+ Your changes are now live!
91
+ ```
92
+
93
+ #### /twin status [server]
94
+ Show status of twin servers.
95
+
96
+ If server specified, show detailed view.
97
+ If no server, show summary of all.
98
+
99
+ Example (all):
100
+ ```
101
+ MCP Twin Status
102
+
103
+ inbox-collab
104
+ Active: A (8101) healthy
105
+ Standby: B (8102) healthy
106
+ Reloads: 5 | Swaps: 3
107
+
108
+ phi-proxy
109
+ Active: B (8104) healthy
110
+ Standby: A (8103) healthy
111
+ Reloads: 2 | Swaps: 1
112
+
113
+ Not running: zti-server, fsl-compression
114
+ ```
115
+
116
+ #### /twin stop [server]
117
+ Stop twin servers.
118
+
119
+ Use `--all` to stop all twins.
120
+
121
+ Example:
122
+ ```
123
+ Stopped inbox-collab twins
124
+
125
+ Killed: PID 12345, 12346
126
+ Ports freed: 8101, 8102
127
+ ```
128
+
129
+ ### Auto-Detection
130
+
131
+ Read MCP servers from Claude config:
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "inbox-collab": {
136
+ "command": "python3",
137
+ "args": ["/path/to/server.py"]
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ Extract script path from args, use for file watching and process management.
144
+
145
+ ### Error Handling
146
+
147
+ **Server not found:**
148
+ ```
149
+ Unknown server: foo-bar
150
+
151
+ Available servers:
152
+ inbox-collab, phi-proxy, zti-server
153
+
154
+ Run: /twin start <server>
155
+ ```
156
+
157
+ **Twins not running:**
158
+ ```
159
+ No twins running for inbox-collab
160
+
161
+ Start first: /twin start inbox-collab
162
+ ```
163
+
164
+ **Standby unhealthy:**
165
+ ```
166
+ Cannot swap - standby is unhealthy
167
+
168
+ Standby (B): port 8102
169
+ State: failed
170
+
171
+ Fix the issue and run: /twin reload inbox-collab
172
+ ```
173
+
174
+ ### Pro Features (Coming Soon)
175
+
176
+ When user runs 5+ reloads in a session:
177
+ ```
178
+ You're iterating fast! Prax Chat Pro coming soon:
179
+
180
+ - Auto-reload on file save
181
+ - Auto-swap option
182
+ - Health dashboard
183
+ - Rollback support
184
+
185
+ [Notify Me] [Maybe Later]
186
+ ```