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.
- package/.claude-plugin/marketplace.json +23 -0
- package/LICENSE +21 -0
- package/PLUGIN_SPEC.md +388 -0
- package/README.md +306 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +215 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-detector.d.ts +53 -0
- package/dist/config-detector.d.ts.map +1 -0
- package/dist/config-detector.js +319 -0
- package/dist/config-detector.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +272 -0
- package/dist/index.js.map +1 -0
- package/dist/twin-manager.d.ts +40 -0
- package/dist/twin-manager.d.ts.map +1 -0
- package/dist/twin-manager.js +518 -0
- package/dist/twin-manager.js.map +1 -0
- package/examples/http-server.py +247 -0
- package/package.json +97 -0
- package/skills/twin.md +186 -0
- package/src/cli.ts +217 -0
- package/src/config-detector.ts +340 -0
- package/src/index.ts +309 -0
- package/src/twin-manager.ts +596 -0
- package/tsconfig.json +19 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Twin Plugin - Entry Point
|
|
3
|
+
* Zero-downtime MCP server updates for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Powered by Prax - https://prax.chat
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getTwinManager, MCPTwinManager } from './twin-manager';
|
|
9
|
+
import { detectMCPServers, generateTwinConfig, autoConfigureTwins } from './config-detector';
|
|
10
|
+
|
|
11
|
+
export { MCPTwinManager, getTwinManager };
|
|
12
|
+
export { detectMCPServers, generateTwinConfig, autoConfigureTwins };
|
|
13
|
+
|
|
14
|
+
// Branding
|
|
15
|
+
const PRAX_FOOTER = '\n─────────────────────────────────────────\n⚡ Powered by Prax Chat | prax.chat/mcp-twin';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plugin Commands
|
|
19
|
+
*/
|
|
20
|
+
export const commands = {
|
|
21
|
+
/**
|
|
22
|
+
* /twin start [server]
|
|
23
|
+
*/
|
|
24
|
+
async start(serverName?: string): Promise<string> {
|
|
25
|
+
const manager = getTwinManager();
|
|
26
|
+
|
|
27
|
+
if (!serverName) {
|
|
28
|
+
// Show available servers
|
|
29
|
+
const status = await manager.status();
|
|
30
|
+
const available = status.available || [];
|
|
31
|
+
|
|
32
|
+
if (available.length === 0) {
|
|
33
|
+
return `No MCP servers configured.
|
|
34
|
+
|
|
35
|
+
Run auto-detect to find servers:
|
|
36
|
+
/twin detect
|
|
37
|
+
|
|
38
|
+
Or add manually:
|
|
39
|
+
/twin add <name> <script-path>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `Available servers:
|
|
43
|
+
${available.map((s: string) => ` - ${s}`).join('\n')}
|
|
44
|
+
|
|
45
|
+
Start twins:
|
|
46
|
+
/twin start <server-name>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = await manager.startTwins(serverName);
|
|
50
|
+
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
return `Failed to start twins: ${result.error}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `Started twins for ${serverName}
|
|
56
|
+
|
|
57
|
+
Server A: port ${result.pidA ? result.statusA : 'failed'} ${result.statusA === 'running' ? '●' : '○'}
|
|
58
|
+
Server B: port ${result.pidB ? result.statusB : 'failed'} ${result.statusB === 'running' ? '●' : '○'}
|
|
59
|
+
Active: A
|
|
60
|
+
|
|
61
|
+
${result.hint}${PRAX_FOOTER}`;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* /twin stop [server]
|
|
66
|
+
*/
|
|
67
|
+
async stop(serverName?: string): Promise<string> {
|
|
68
|
+
const manager = getTwinManager();
|
|
69
|
+
|
|
70
|
+
if (!serverName) {
|
|
71
|
+
return `Usage: /twin stop <server-name>
|
|
72
|
+
|
|
73
|
+
Or stop all: /twin stop --all`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (serverName === '--all') {
|
|
77
|
+
const status = await manager.status();
|
|
78
|
+
const running = Object.keys(status.twins || {});
|
|
79
|
+
|
|
80
|
+
if (running.length === 0) {
|
|
81
|
+
return 'No twins running.';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const results: string[] = [];
|
|
85
|
+
for (const name of running) {
|
|
86
|
+
const result = await manager.stopTwins(name);
|
|
87
|
+
results.push(` ${name}: ${result.ok ? 'stopped' : 'failed'}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `Stopped all twins:\n${results.join('\n')}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await manager.stopTwins(serverName);
|
|
94
|
+
|
|
95
|
+
if (!result.ok) {
|
|
96
|
+
return `Failed to stop: ${result.error}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `Stopped ${serverName} twins
|
|
100
|
+
|
|
101
|
+
Server A: stopped
|
|
102
|
+
Server B: stopped`;
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* /twin reload <server>
|
|
107
|
+
*/
|
|
108
|
+
async reload(serverName: string): Promise<string> {
|
|
109
|
+
if (!serverName) {
|
|
110
|
+
return 'Usage: /twin reload <server-name>';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const manager = getTwinManager();
|
|
114
|
+
const result = await manager.reloadStandby(serverName);
|
|
115
|
+
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
return `Reload failed: ${result.error}
|
|
118
|
+
|
|
119
|
+
${result.hint || ''}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `Reloaded ${serverName} standby
|
|
123
|
+
|
|
124
|
+
${result.reloaded}
|
|
125
|
+
Health: ${result.healthy ? 'passing ●' : 'FAILED ○'}
|
|
126
|
+
Reload count: ${result.reloadCount}
|
|
127
|
+
|
|
128
|
+
${result.hint}`;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* /twin swap <server>
|
|
133
|
+
*/
|
|
134
|
+
async swap(serverName: string): Promise<string> {
|
|
135
|
+
if (!serverName) {
|
|
136
|
+
return 'Usage: /twin swap <server-name>';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const manager = getTwinManager();
|
|
140
|
+
const result = await manager.swapActive(serverName);
|
|
141
|
+
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
return `Swap failed: ${result.error}
|
|
144
|
+
|
|
145
|
+
${result.hint || ''}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return `Swapped ${serverName}
|
|
149
|
+
|
|
150
|
+
Previous: ${result.previousActive}
|
|
151
|
+
Now active: ${result.newActive}
|
|
152
|
+
|
|
153
|
+
${result.hint}${PRAX_FOOTER}`;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* /twin status [server]
|
|
158
|
+
*/
|
|
159
|
+
async status(serverName?: string): Promise<string> {
|
|
160
|
+
const manager = getTwinManager();
|
|
161
|
+
const result = await manager.status(serverName);
|
|
162
|
+
|
|
163
|
+
if (!result.ok) {
|
|
164
|
+
return `Error: ${result.error}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (serverName) {
|
|
168
|
+
// Detailed single server status
|
|
169
|
+
const { serverA, serverB, active, reloadCount, lastSwap } = result;
|
|
170
|
+
|
|
171
|
+
return `${serverName} Twin Status
|
|
172
|
+
${'═'.repeat(40)}
|
|
173
|
+
|
|
174
|
+
Server A (port ${serverA.port})
|
|
175
|
+
State: ${serverA.state}
|
|
176
|
+
Health: ${serverA.healthy ? 'healthy ●' : 'unhealthy ○'}
|
|
177
|
+
${serverA.isActive ? '← ACTIVE' : ' standby'}
|
|
178
|
+
|
|
179
|
+
Server B (port ${serverB.port})
|
|
180
|
+
State: ${serverB.state}
|
|
181
|
+
Health: ${serverB.healthy ? 'healthy ●' : 'unhealthy ○'}
|
|
182
|
+
${serverB.isActive ? '← ACTIVE' : ' standby'}
|
|
183
|
+
|
|
184
|
+
Reloads: ${reloadCount}
|
|
185
|
+
Last swap: ${lastSwap ? new Date(lastSwap).toLocaleTimeString() : 'never'}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// All twins summary
|
|
189
|
+
const twins = result.twins || {};
|
|
190
|
+
const twinNames = Object.keys(twins);
|
|
191
|
+
|
|
192
|
+
if (twinNames.length === 0) {
|
|
193
|
+
const available = result.available || [];
|
|
194
|
+
return `No twins running.
|
|
195
|
+
|
|
196
|
+
Available servers:
|
|
197
|
+
${available.map((s: string) => ` - ${s}`).join('\n')}
|
|
198
|
+
|
|
199
|
+
Start with: /twin start <server>`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let output = `MCP Twin Status
|
|
203
|
+
${'═'.repeat(40)}
|
|
204
|
+
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
for (const [name, info] of Object.entries(twins) as [string, any][]) {
|
|
208
|
+
const activeLabel = info.active.toUpperCase();
|
|
209
|
+
const [healthA, healthB] = info.healthy;
|
|
210
|
+
|
|
211
|
+
output += `${name}
|
|
212
|
+
Active: ${activeLabel} (${info.ports[info.active === 'a' ? 0 : 1]}) ${healthA || healthB ? '●' : '○'}
|
|
213
|
+
Standby: ${info.active === 'a' ? 'B' : 'A'} (${info.ports[info.active === 'a' ? 1 : 0]}) ${(info.active === 'a' ? healthB : healthA) ? '●' : '○'}
|
|
214
|
+
Reloads: ${info.reloadCount}
|
|
215
|
+
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const notRunning = (result.available || []).filter((s: string) => !twins[s]);
|
|
220
|
+
if (notRunning.length > 0) {
|
|
221
|
+
output += `Not running: ${notRunning.join(', ')}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return output;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* /twin detect - Auto-detect MCP servers
|
|
229
|
+
*/
|
|
230
|
+
async detect(): Promise<string> {
|
|
231
|
+
const result = autoConfigureTwins();
|
|
232
|
+
|
|
233
|
+
if (result.detected === 0) {
|
|
234
|
+
return `No MCP servers found in Claude config.
|
|
235
|
+
|
|
236
|
+
Add servers manually:
|
|
237
|
+
/twin add <name> <script-path> <port1> <port2>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return `Detected ${result.detected} MCP servers:
|
|
241
|
+
${result.servers.map(s => ` - ${s}`).join('\n')}
|
|
242
|
+
|
|
243
|
+
${result.configured} configured for twins.
|
|
244
|
+
|
|
245
|
+
Start twins:
|
|
246
|
+
/twin start <server-name>`;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* /twin help
|
|
251
|
+
*/
|
|
252
|
+
help(): string {
|
|
253
|
+
return `MCP Twin - Zero-Downtime Server Updates
|
|
254
|
+
${'═'.repeat(44)}
|
|
255
|
+
|
|
256
|
+
Commands:
|
|
257
|
+
/twin start [server] Start twin servers
|
|
258
|
+
/twin stop [server] Stop twin servers
|
|
259
|
+
/twin reload <server> Reload standby with new code
|
|
260
|
+
/twin swap <server> Switch traffic to standby
|
|
261
|
+
/twin status [server] Show twin status
|
|
262
|
+
/twin detect Auto-detect MCP servers
|
|
263
|
+
/twin help Show this help
|
|
264
|
+
|
|
265
|
+
Workflow:
|
|
266
|
+
1. /twin start my-server → Start A (active) + B (standby)
|
|
267
|
+
2. Edit your server code
|
|
268
|
+
3. /twin reload my-server → Update standby B
|
|
269
|
+
4. /twin swap my-server → Switch to B
|
|
270
|
+
5. Keep coding - no restart needed!
|
|
271
|
+
${PRAX_FOOTER}
|
|
272
|
+
|
|
273
|
+
Pro features coming soon: prax.chat/mcp-twin/pro`;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Main command router
|
|
279
|
+
*/
|
|
280
|
+
export async function handleCommand(args: string[]): Promise<string> {
|
|
281
|
+
const [subcommand, ...rest] = args;
|
|
282
|
+
|
|
283
|
+
switch (subcommand?.toLowerCase()) {
|
|
284
|
+
case 'start':
|
|
285
|
+
return commands.start(rest[0]);
|
|
286
|
+
case 'stop':
|
|
287
|
+
return commands.stop(rest[0]);
|
|
288
|
+
case 'reload':
|
|
289
|
+
return commands.reload(rest[0]);
|
|
290
|
+
case 'swap':
|
|
291
|
+
return commands.swap(rest[0]);
|
|
292
|
+
case 'status':
|
|
293
|
+
return commands.status(rest[0]);
|
|
294
|
+
case 'detect':
|
|
295
|
+
return commands.detect();
|
|
296
|
+
case 'help':
|
|
297
|
+
case undefined:
|
|
298
|
+
return commands.help();
|
|
299
|
+
default:
|
|
300
|
+
return `Unknown command: ${subcommand}\n\nRun /twin help for usage.`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default {
|
|
305
|
+
commands,
|
|
306
|
+
handleCommand,
|
|
307
|
+
getTwinManager,
|
|
308
|
+
detectMCPServers
|
|
309
|
+
};
|