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/cli.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Twin CLI - Universal zero-downtime MCP server management
|
|
4
|
+
*
|
|
5
|
+
* Works with ANY MCP client: Claude Desktop, VS Code, Cursor, Cline, etc.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx mcp-twin start my-server
|
|
9
|
+
* npx mcp-twin reload my-server
|
|
10
|
+
* npx mcp-twin swap my-server
|
|
11
|
+
* npx mcp-twin status
|
|
12
|
+
*
|
|
13
|
+
* Powered by Prax Chat - https://prax.chat
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getTwinManager } from './twin-manager';
|
|
17
|
+
import { autoConfigureTwins } from './config-detector';
|
|
18
|
+
|
|
19
|
+
const PRAX_FOOTER = '\n─────────────────────────────────────────\n⚡ Powered by Prax Chat | prax.chat/mcp-twin';
|
|
20
|
+
|
|
21
|
+
const VERSION = '1.2.0';
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const command = args[0]?.toLowerCase();
|
|
26
|
+
const serverName = args[1];
|
|
27
|
+
|
|
28
|
+
const manager = getTwinManager();
|
|
29
|
+
|
|
30
|
+
switch (command) {
|
|
31
|
+
case 'start': {
|
|
32
|
+
if (!serverName) {
|
|
33
|
+
const status = await manager.status();
|
|
34
|
+
console.log('Available servers:');
|
|
35
|
+
(status.available || []).forEach((s: string) => console.log(` - ${s}`));
|
|
36
|
+
console.log('\nUsage: mcp-twin start <server-name>');
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
const result = await manager.startTwins(serverName);
|
|
40
|
+
if (result.ok) {
|
|
41
|
+
console.log(`✓ Started twins for ${serverName}`);
|
|
42
|
+
console.log(` Server A: port ${result.portA} (${result.statusA})`);
|
|
43
|
+
console.log(` Server B: port ${result.portB} (${result.statusB})`);
|
|
44
|
+
console.log(` Active: A`);
|
|
45
|
+
} else {
|
|
46
|
+
console.error(`✗ Failed: ${result.error}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
console.log(PRAX_FOOTER);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'stop': {
|
|
54
|
+
if (!serverName) {
|
|
55
|
+
console.log('Usage: mcp-twin stop <server-name>');
|
|
56
|
+
console.log(' mcp-twin stop --all');
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (serverName === '--all') {
|
|
60
|
+
const status = await manager.status();
|
|
61
|
+
for (const name of Object.keys(status.twins || {})) {
|
|
62
|
+
await manager.stopTwins(name);
|
|
63
|
+
console.log(`✓ Stopped ${name}`);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const result = await manager.stopTwins(serverName);
|
|
67
|
+
if (result.ok) {
|
|
68
|
+
console.log(`✓ Stopped ${serverName} twins`);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`✗ Failed: ${result.error}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'reload': {
|
|
78
|
+
if (!serverName) {
|
|
79
|
+
console.log('Usage: mcp-twin reload <server-name>');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
console.log(`Reloading ${serverName} standby...`);
|
|
83
|
+
const result = await manager.reloadStandby(serverName);
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
console.log(`✓ Reloaded ${result.reloaded}`);
|
|
86
|
+
console.log(` Health: ${result.healthy ? 'passing ●' : 'FAILED ○'}`);
|
|
87
|
+
console.log(` Reload count: ${result.reloadCount}`);
|
|
88
|
+
if (result.healthy) {
|
|
89
|
+
console.log(`\nReady to swap: mcp-twin swap ${serverName}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`✗ Failed: ${result.error}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
console.log(PRAX_FOOTER);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'swap': {
|
|
100
|
+
if (!serverName) {
|
|
101
|
+
console.log('Usage: mcp-twin swap <server-name>');
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const result = await manager.swapActive(serverName);
|
|
105
|
+
if (result.ok) {
|
|
106
|
+
console.log(`✓ Swapped ${serverName}`);
|
|
107
|
+
console.log(` Previous: ${result.previousActive}`);
|
|
108
|
+
console.log(` Now active: ${result.newActive}`);
|
|
109
|
+
} else {
|
|
110
|
+
console.error(`✗ Failed: ${result.error}`);
|
|
111
|
+
if (result.hint) console.log(` Hint: ${result.hint}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
console.log(PRAX_FOOTER);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'status': {
|
|
119
|
+
const result = await manager.status(serverName);
|
|
120
|
+
if (serverName && result.ok) {
|
|
121
|
+
console.log(`${serverName} Twin Status`);
|
|
122
|
+
console.log('═'.repeat(40));
|
|
123
|
+
console.log(`\nServer A (port ${result.serverA.port})`);
|
|
124
|
+
console.log(` State: ${result.serverA.state}`);
|
|
125
|
+
console.log(` Health: ${result.serverA.healthy ? 'healthy ●' : 'unhealthy ○'}`);
|
|
126
|
+
console.log(` ${result.serverA.isActive ? '← ACTIVE' : ' standby'}`);
|
|
127
|
+
console.log(`\nServer B (port ${result.serverB.port})`);
|
|
128
|
+
console.log(` State: ${result.serverB.state}`);
|
|
129
|
+
console.log(` Health: ${result.serverB.healthy ? 'healthy ●' : 'unhealthy ○'}`);
|
|
130
|
+
console.log(` ${result.serverB.isActive ? '← ACTIVE' : ' standby'}`);
|
|
131
|
+
console.log(`\nReloads: ${result.reloadCount}`);
|
|
132
|
+
} else if (result.ok) {
|
|
133
|
+
console.log('MCP Twin Status');
|
|
134
|
+
console.log('═'.repeat(40));
|
|
135
|
+
const twins = result.twins || {};
|
|
136
|
+
if (Object.keys(twins).length === 0) {
|
|
137
|
+
console.log('\nNo twins running.');
|
|
138
|
+
console.log('\nAvailable servers:');
|
|
139
|
+
(result.available || []).forEach((s: string) => console.log(` - ${s}`));
|
|
140
|
+
} else {
|
|
141
|
+
for (const [name, info] of Object.entries(twins) as [string, any][]) {
|
|
142
|
+
const activePort = info.ports[info.active === 'a' ? 0 : 1];
|
|
143
|
+
console.log(`\n${name}`);
|
|
144
|
+
console.log(` Active: ${info.active.toUpperCase()} (port ${activePort}) ${info.healthy[info.active === 'a' ? 0 : 1] ? '●' : '○'}`);
|
|
145
|
+
console.log(` Reloads: ${info.reloadCount}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'detect': {
|
|
153
|
+
console.log('Detecting MCP servers...');
|
|
154
|
+
const result = autoConfigureTwins();
|
|
155
|
+
if (result.detected === 0) {
|
|
156
|
+
console.log('No MCP servers found.');
|
|
157
|
+
console.log('\nSearched:');
|
|
158
|
+
console.log(' - Claude Desktop: ~/Library/Application Support/Claude/');
|
|
159
|
+
console.log(' - Claude Code: ~/.claude/');
|
|
160
|
+
console.log(' - VS Code: .vscode/mcp.json');
|
|
161
|
+
console.log(' - Cursor: ~/.cursor/mcp.json');
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`✓ Detected ${result.detected} MCP servers:`);
|
|
164
|
+
result.servers.forEach((s: string) => console.log(` - ${s}`));
|
|
165
|
+
console.log(`\n${result.configured} configured for twins.`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'version':
|
|
171
|
+
case '-v':
|
|
172
|
+
case '--version': {
|
|
173
|
+
console.log(`MCP Twin v${VERSION}`);
|
|
174
|
+
console.log('Powered by Prax Chat - https://prax.chat');
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'help':
|
|
179
|
+
case '-h':
|
|
180
|
+
case '--help':
|
|
181
|
+
default: {
|
|
182
|
+
console.log(`
|
|
183
|
+
MCP Twin v${VERSION} - Zero-Downtime MCP Server Updates
|
|
184
|
+
${'═'.repeat(50)}
|
|
185
|
+
|
|
186
|
+
Works with: Claude Desktop, VS Code, Cursor, Cline, Windsurf
|
|
187
|
+
|
|
188
|
+
Commands:
|
|
189
|
+
mcp-twin start <server> Start twin servers
|
|
190
|
+
mcp-twin stop <server> Stop twin servers (--all for all)
|
|
191
|
+
mcp-twin reload <server> Reload standby with new code
|
|
192
|
+
mcp-twin swap <server> Switch traffic to standby
|
|
193
|
+
mcp-twin status [server] Show twin status
|
|
194
|
+
mcp-twin detect Auto-detect MCP servers
|
|
195
|
+
mcp-twin help Show this help
|
|
196
|
+
|
|
197
|
+
Workflow:
|
|
198
|
+
1. mcp-twin start my-server → Start A (active) + B (standby)
|
|
199
|
+
2. Edit your server code
|
|
200
|
+
3. mcp-twin reload my-server → Update standby B
|
|
201
|
+
4. mcp-twin swap my-server → Switch to B (zero downtime!)
|
|
202
|
+
|
|
203
|
+
Config: ~/.mcp-twin/config.json
|
|
204
|
+
Logs: ~/.mcp-twin/logs/
|
|
205
|
+
${PRAX_FOOTER}
|
|
206
|
+
|
|
207
|
+
Pro features: prax.chat/mcp-twin/pro
|
|
208
|
+
`);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main().catch(err => {
|
|
215
|
+
console.error('Error:', err.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Detector - Auto-detect MCP servers from multiple MCP clients
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Claude Desktop
|
|
6
|
+
* - Claude Code
|
|
7
|
+
* - VS Code (GitHub Copilot)
|
|
8
|
+
* - Cursor
|
|
9
|
+
* - Cline
|
|
10
|
+
* - Windsurf
|
|
11
|
+
*
|
|
12
|
+
* Powered by Prax Chat - https://prax.chat
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
|
|
19
|
+
interface MCPServerEntry {
|
|
20
|
+
command: string;
|
|
21
|
+
args?: string[];
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MCPConfig {
|
|
26
|
+
mcpServers?: Record<string, MCPServerEntry>;
|
|
27
|
+
servers?: Record<string, MCPServerEntry>; // VS Code/Cursor format
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DetectedServer {
|
|
31
|
+
name: string;
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
scriptPath: string | null;
|
|
35
|
+
language: 'python' | 'node' | 'unknown';
|
|
36
|
+
source: string; // Which client config it came from
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all possible MCP config paths for different clients
|
|
41
|
+
*/
|
|
42
|
+
function getAllConfigPaths(): { client: string; path: string }[] {
|
|
43
|
+
const platform = os.platform();
|
|
44
|
+
const home = os.homedir();
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
|
|
47
|
+
const paths: { client: string; path: string }[] = [];
|
|
48
|
+
|
|
49
|
+
// Claude Desktop
|
|
50
|
+
if (platform === 'darwin') {
|
|
51
|
+
paths.push({
|
|
52
|
+
client: 'Claude Desktop',
|
|
53
|
+
path: path.join(home, 'Library/Application Support/Claude/claude_desktop_config.json')
|
|
54
|
+
});
|
|
55
|
+
} else if (platform === 'win32') {
|
|
56
|
+
paths.push({
|
|
57
|
+
client: 'Claude Desktop',
|
|
58
|
+
path: path.join(home, 'AppData/Roaming/Claude/claude_desktop_config.json')
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
paths.push({
|
|
62
|
+
client: 'Claude Desktop',
|
|
63
|
+
path: path.join(home, '.config/claude/claude_desktop_config.json')
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Claude Code
|
|
68
|
+
paths.push({
|
|
69
|
+
client: 'Claude Code',
|
|
70
|
+
path: path.join(home, '.claude/claude_desktop_config.json')
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// VS Code - workspace
|
|
74
|
+
paths.push({
|
|
75
|
+
client: 'VS Code (workspace)',
|
|
76
|
+
path: path.join(cwd, '.vscode/mcp.json')
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// VS Code - user settings (macOS)
|
|
80
|
+
if (platform === 'darwin') {
|
|
81
|
+
paths.push({
|
|
82
|
+
client: 'VS Code (user)',
|
|
83
|
+
path: path.join(home, 'Library/Application Support/Code/User/settings.json')
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Cursor - workspace
|
|
88
|
+
paths.push({
|
|
89
|
+
client: 'Cursor (workspace)',
|
|
90
|
+
path: path.join(cwd, '.cursor/mcp.json')
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Cursor - global
|
|
94
|
+
paths.push({
|
|
95
|
+
client: 'Cursor (global)',
|
|
96
|
+
path: path.join(home, '.cursor/mcp.json')
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Windsurf
|
|
100
|
+
paths.push({
|
|
101
|
+
client: 'Windsurf',
|
|
102
|
+
path: path.join(home, '.windsurf/mcp.json')
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Cline (VS Code extension, uses VS Code settings)
|
|
106
|
+
// Already covered by VS Code paths
|
|
107
|
+
|
|
108
|
+
return paths;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get path to Claude desktop config (legacy, for backwards compatibility)
|
|
113
|
+
*/
|
|
114
|
+
function getClaudeConfigPath(): string {
|
|
115
|
+
const platform = os.platform();
|
|
116
|
+
|
|
117
|
+
if (platform === 'darwin') {
|
|
118
|
+
return path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
119
|
+
} else if (platform === 'win32') {
|
|
120
|
+
return path.join(os.homedir(), 'AppData/Roaming/Claude/claude_desktop_config.json');
|
|
121
|
+
} else {
|
|
122
|
+
// Linux
|
|
123
|
+
return path.join(os.homedir(), '.config/claude/claude_desktop_config.json');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract script path from command args
|
|
129
|
+
*/
|
|
130
|
+
function extractScriptPath(command: string, args: string[]): string | null {
|
|
131
|
+
// Common patterns:
|
|
132
|
+
// python3 /path/to/server.py
|
|
133
|
+
// node /path/to/server.js
|
|
134
|
+
// npx ts-node /path/to/server.ts
|
|
135
|
+
|
|
136
|
+
for (const arg of args) {
|
|
137
|
+
// Skip flags
|
|
138
|
+
if (arg.startsWith('-')) continue;
|
|
139
|
+
|
|
140
|
+
// Check if it's a file path
|
|
141
|
+
const expanded = arg.replace('~', os.homedir());
|
|
142
|
+
if (fs.existsSync(expanded)) {
|
|
143
|
+
return expanded;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check common extensions
|
|
147
|
+
if (arg.endsWith('.py') || arg.endsWith('.js') || arg.endsWith('.ts')) {
|
|
148
|
+
return arg.replace('~', os.homedir());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect language from command
|
|
157
|
+
*/
|
|
158
|
+
function detectLanguage(command: string): 'python' | 'node' | 'unknown' {
|
|
159
|
+
const cmd = command.toLowerCase();
|
|
160
|
+
|
|
161
|
+
if (cmd.includes('python') || cmd.includes('python3')) {
|
|
162
|
+
return 'python';
|
|
163
|
+
}
|
|
164
|
+
if (cmd.includes('node') || cmd.includes('npx') || cmd.includes('tsx')) {
|
|
165
|
+
return 'node';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return 'unknown';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Assign deterministic ports based on server name
|
|
173
|
+
*/
|
|
174
|
+
export function assignPorts(serverName: string): [number, number] {
|
|
175
|
+
// Simple hash function
|
|
176
|
+
let hash = 0;
|
|
177
|
+
for (let i = 0; i < serverName.length; i++) {
|
|
178
|
+
const char = serverName.charCodeAt(i);
|
|
179
|
+
hash = ((hash << 5) - hash) + char;
|
|
180
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Map to port range 8100-8298 (99 server pairs)
|
|
184
|
+
const basePort = 8100 + (Math.abs(hash) % 100) * 2;
|
|
185
|
+
return [basePort, basePort + 1];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Detect MCP servers from all supported MCP clients
|
|
190
|
+
*/
|
|
191
|
+
export function detectMCPServers(): DetectedServer[] {
|
|
192
|
+
const allPaths = getAllConfigPaths();
|
|
193
|
+
const servers: DetectedServer[] = [];
|
|
194
|
+
const seen = new Set<string>(); // Dedupe by server name
|
|
195
|
+
|
|
196
|
+
for (const { client, path: configPath } of allPaths) {
|
|
197
|
+
if (!fs.existsSync(configPath)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const config: MCPConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
203
|
+
|
|
204
|
+
// Support both mcpServers (Claude) and servers (VS Code/Cursor) formats
|
|
205
|
+
const mcpServers = config.mcpServers || config.servers || {};
|
|
206
|
+
|
|
207
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
208
|
+
// Skip if already seen (first config wins)
|
|
209
|
+
if (seen.has(name)) continue;
|
|
210
|
+
seen.add(name);
|
|
211
|
+
|
|
212
|
+
const args = entry.args || [];
|
|
213
|
+
const scriptPath = extractScriptPath(entry.command, args);
|
|
214
|
+
const language = detectLanguage(entry.command);
|
|
215
|
+
|
|
216
|
+
servers.push({
|
|
217
|
+
name,
|
|
218
|
+
command: entry.command,
|
|
219
|
+
args,
|
|
220
|
+
scriptPath,
|
|
221
|
+
language,
|
|
222
|
+
source: client
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// Skip invalid configs silently
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return servers;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Detect MCP servers from Claude config only (legacy)
|
|
236
|
+
*/
|
|
237
|
+
export function detectMCPServersFromClaude(): DetectedServer[] {
|
|
238
|
+
const configPath = getClaudeConfigPath();
|
|
239
|
+
|
|
240
|
+
if (!fs.existsSync(configPath)) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const config: MCPConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
246
|
+
const servers: DetectedServer[] = [];
|
|
247
|
+
|
|
248
|
+
if (!config.mcpServers) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
253
|
+
const args = entry.args || [];
|
|
254
|
+
const scriptPath = extractScriptPath(entry.command, args);
|
|
255
|
+
const language = detectLanguage(entry.command);
|
|
256
|
+
|
|
257
|
+
servers.push({
|
|
258
|
+
name,
|
|
259
|
+
command: entry.command,
|
|
260
|
+
args,
|
|
261
|
+
scriptPath,
|
|
262
|
+
language,
|
|
263
|
+
source: 'Claude Desktop'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return servers;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate twin config from detected servers
|
|
275
|
+
*/
|
|
276
|
+
export function generateTwinConfig(servers: DetectedServer[]): Record<string, any> {
|
|
277
|
+
const config: Record<string, any> = {};
|
|
278
|
+
|
|
279
|
+
for (const server of servers) {
|
|
280
|
+
if (!server.scriptPath) {
|
|
281
|
+
console.log(`[ConfigDetector] Skipping ${server.name}: no script path found`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const [portA, portB] = assignPorts(server.name);
|
|
286
|
+
|
|
287
|
+
config[server.name] = {
|
|
288
|
+
script: server.scriptPath,
|
|
289
|
+
ports: [portA, portB],
|
|
290
|
+
healthEndpoint: '/health',
|
|
291
|
+
startupTimeout: 10,
|
|
292
|
+
python: server.language === 'python' ? server.command : 'python3'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return config;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Auto-detect and update twin config
|
|
301
|
+
*/
|
|
302
|
+
export function autoConfigureTwins(): { detected: number; configured: number; servers: string[] } {
|
|
303
|
+
const detected = detectMCPServers();
|
|
304
|
+
const twinConfig = generateTwinConfig(detected);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
detected: detected.length,
|
|
308
|
+
configured: Object.keys(twinConfig).length,
|
|
309
|
+
servers: Object.keys(twinConfig)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// CLI test
|
|
314
|
+
if (require.main === module) {
|
|
315
|
+
console.log('Detecting MCP servers...\n');
|
|
316
|
+
|
|
317
|
+
const servers = detectMCPServers();
|
|
318
|
+
|
|
319
|
+
if (servers.length === 0) {
|
|
320
|
+
console.log('No MCP servers found in Claude config.');
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`Found ${servers.length} servers:\n`);
|
|
323
|
+
|
|
324
|
+
for (const server of servers) {
|
|
325
|
+
console.log(` ${server.name}`);
|
|
326
|
+
console.log(` Command: ${server.command}`);
|
|
327
|
+
console.log(` Script: ${server.scriptPath || '(not found)'}`);
|
|
328
|
+
console.log(` Language: ${server.language}`);
|
|
329
|
+
console.log(` Ports: ${assignPorts(server.name).join(', ')}`);
|
|
330
|
+
console.log();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export default {
|
|
336
|
+
detectMCPServers,
|
|
337
|
+
generateTwinConfig,
|
|
338
|
+
autoConfigureTwins,
|
|
339
|
+
assignPorts
|
|
340
|
+
};
|