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/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
+ };