@maplezzk/mcps 1.0.4 → 1.0.14
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/dist/commands/call.js +9 -50
- package/dist/commands/daemon.js +138 -21
- package/dist/commands/server.js +16 -7
- package/dist/commands/tools.js +34 -32
- package/dist/core/client.js +33 -4
- package/dist/core/constants.js +2 -0
- package/dist/core/daemon-client.js +67 -0
- package/dist/core/pool.js +67 -3
- package/package.json +1 -1
package/dist/commands/call.js
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { configManager } from '../core/config.js';
|
|
3
|
-
import {
|
|
4
|
-
const DAEMON_PORT = 4100;
|
|
5
|
-
async function tryCallDaemon(serverName, toolName, args) {
|
|
6
|
-
try {
|
|
7
|
-
const response = await fetch(`http://localhost:${DAEMON_PORT}/call`, {
|
|
8
|
-
method: 'POST',
|
|
9
|
-
headers: { 'Content-Type': 'application/json' },
|
|
10
|
-
body: JSON.stringify({ server: serverName, tool: toolName, args }),
|
|
11
|
-
});
|
|
12
|
-
if (!response.ok) {
|
|
13
|
-
// If daemon returns error, we might want to show it or fallback?
|
|
14
|
-
// Let's assume 500 means daemon tried and failed, so we shouldn't fallback to local spawn as it might fail same way.
|
|
15
|
-
// But if 404/Connection Refused, then daemon is not running.
|
|
16
|
-
// fetch throws on connection refused.
|
|
17
|
-
const err = await response.json();
|
|
18
|
-
throw new Error(err.error || 'Daemon error');
|
|
19
|
-
}
|
|
20
|
-
const data = await response.json();
|
|
21
|
-
console.log(chalk.green('Tool execution successful (via Daemon):'));
|
|
22
|
-
printResult(data.result);
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
// If connection failed (daemon not running), return false to fallback
|
|
27
|
-
if (error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch failed')) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
// If daemon connected but returned error (e.g. tool failed), rethrow
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
3
|
+
import { DaemonClient } from '../core/daemon-client.js';
|
|
34
4
|
function printResult(result) {
|
|
35
5
|
if (result.content) {
|
|
36
6
|
result.content.forEach((item) => {
|
|
@@ -82,34 +52,23 @@ Notes:
|
|
|
82
52
|
}
|
|
83
53
|
});
|
|
84
54
|
}
|
|
85
|
-
//
|
|
86
|
-
try {
|
|
87
|
-
const handled = await tryCallDaemon(serverName, toolName, params);
|
|
88
|
-
if (handled)
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
console.error(chalk.red(`Daemon call failed: ${error.message}`));
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
// 2. Fallback to standalone execution
|
|
55
|
+
// Check if server exists in config first
|
|
96
56
|
const serverConfig = configManager.getServer(serverName);
|
|
97
57
|
if (!serverConfig) {
|
|
98
|
-
console.error(chalk.red(`Server "${serverName}" not found.`));
|
|
58
|
+
console.error(chalk.red(`Server "${serverName}" not found in config.`));
|
|
99
59
|
process.exit(1);
|
|
100
60
|
}
|
|
101
|
-
const client = new McpClientService();
|
|
102
61
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
62
|
+
// Auto-start daemon if needed
|
|
63
|
+
await DaemonClient.ensureDaemon();
|
|
64
|
+
// Execute via daemon
|
|
65
|
+
const result = await DaemonClient.executeTool(serverName, toolName, params);
|
|
105
66
|
console.log(chalk.green('Tool execution successful:'));
|
|
106
67
|
printResult(result);
|
|
107
68
|
}
|
|
108
69
|
catch (error) {
|
|
109
|
-
console.error(chalk.red(`
|
|
110
|
-
|
|
111
|
-
finally {
|
|
112
|
-
await client.close();
|
|
70
|
+
console.error(chalk.red(`Execution failed: ${error.message}`));
|
|
71
|
+
process.exit(1);
|
|
113
72
|
}
|
|
114
73
|
});
|
|
115
74
|
};
|
package/dist/commands/daemon.js
CHANGED
|
@@ -1,40 +1,118 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
1
2
|
import http from 'http';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { connectionPool } from '../core/pool.js';
|
|
4
5
|
import { createRequire } from 'module';
|
|
6
|
+
import { DAEMON_PORT } from '../core/constants.js';
|
|
5
7
|
const require = createRequire(import.meta.url);
|
|
6
8
|
const pkg = require('../../package.json');
|
|
7
|
-
const PORT = 4100;
|
|
8
9
|
export const registerDaemonCommand = (program) => {
|
|
9
10
|
const daemonCmd = program.command('daemon')
|
|
10
|
-
.description('Manage the mcps daemon')
|
|
11
|
-
|
|
11
|
+
.description('Manage the mcps daemon')
|
|
12
|
+
.usage('start|stop|restart|status'); // Simplify usage display
|
|
13
|
+
daemonCmd.command('start', { isDefault: true, hidden: true }) // Hide default start from help but keep functionality
|
|
12
14
|
.description('Start the daemon (default)')
|
|
13
|
-
.option('-p, --port <number>', 'Port to listen on', String(
|
|
15
|
+
.option('-p, --port <number>', 'Port to listen on', String(DAEMON_PORT))
|
|
14
16
|
.action(async (options) => {
|
|
15
17
|
const port = parseInt(options.port);
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Check if already running
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`http://localhost:${port}/status`);
|
|
21
|
+
if (res.ok) {
|
|
22
|
+
console.log(chalk.yellow(`Daemon is already running on port ${port}.`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Not running, safe to start
|
|
28
|
+
// If we are already detached (indicated by env var), run the server
|
|
29
|
+
if (process.env.MCPS_DAEMON_DETACHED === 'true') {
|
|
30
|
+
startDaemon(port);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Otherwise, spawn a detached process
|
|
34
|
+
console.log(chalk.gray('Starting daemon in background...'));
|
|
35
|
+
const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
|
|
36
|
+
detached: true,
|
|
37
|
+
// Pipe stdout/stderr so we can see initialization logs
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
MCPS_DAEMON_DETACHED: 'true'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// Stream logs to current console while waiting for ready
|
|
45
|
+
if (subprocess.stdout) {
|
|
46
|
+
subprocess.stdout.on('data', (data) => {
|
|
47
|
+
process.stdout.write(chalk.gray(`[Daemon] ${data}`));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (subprocess.stderr) {
|
|
51
|
+
subprocess.stderr.on('data', (data) => {
|
|
52
|
+
process.stderr.write(chalk.red(`[Daemon] ${data}`));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
subprocess.unref();
|
|
56
|
+
// Wait briefly to ensure it started (optional but good UX)
|
|
57
|
+
// We can poll status for a second
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
// Increased timeout to allow for connection initialization
|
|
60
|
+
while (Date.now() - start < 10000) {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`http://localhost:${port}/status`);
|
|
63
|
+
if (res.ok) {
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
if (data.initialized) {
|
|
66
|
+
console.log(chalk.green(`Daemon started successfully on port ${port}.`));
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
await new Promise(r => setTimeout(r, 200));
|
|
73
|
+
}
|
|
74
|
+
console.log(chalk.yellow('Daemon started (async check timeout, but likely running).'));
|
|
75
|
+
}
|
|
21
76
|
});
|
|
22
77
|
daemonCmd.command('stop')
|
|
23
78
|
.description('Stop the running daemon')
|
|
24
79
|
.action(async () => {
|
|
25
80
|
try {
|
|
26
|
-
await fetch(`http://localhost:${
|
|
81
|
+
await fetch(`http://localhost:${DAEMON_PORT}/stop`, { method: 'POST' });
|
|
27
82
|
console.log(chalk.green('Daemon stopped successfully.'));
|
|
28
83
|
}
|
|
29
84
|
catch (e) {
|
|
30
85
|
console.error(chalk.red('Failed to stop daemon. Is it running?'));
|
|
31
86
|
}
|
|
32
87
|
});
|
|
88
|
+
daemonCmd.command('status')
|
|
89
|
+
.description('Check daemon status')
|
|
90
|
+
.action(async () => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`http://localhost:${DAEMON_PORT}/status`);
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
console.log(chalk.green(`Daemon is running (v${data.version})`));
|
|
95
|
+
if (data.connections && data.connections.length > 0) {
|
|
96
|
+
console.log(chalk.bold('\nActive Connections:'));
|
|
97
|
+
data.connections.forEach((conn) => {
|
|
98
|
+
const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : (data.initializing ? '(initializing)' : '(error listing tools)');
|
|
99
|
+
const status = conn.status === 'error' ? chalk.red('[Error]') : '';
|
|
100
|
+
console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(chalk.gray('No active connections.'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(chalk.red('Daemon is not running.'));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
33
111
|
daemonCmd.command('restart [server]')
|
|
34
112
|
.description('Restart the daemon or a specific server connection')
|
|
35
113
|
.action(async (serverName) => {
|
|
36
114
|
try {
|
|
37
|
-
const res = await fetch(`http://localhost:${
|
|
115
|
+
const res = await fetch(`http://localhost:${DAEMON_PORT}/restart`, {
|
|
38
116
|
method: 'POST',
|
|
39
117
|
body: JSON.stringify({ server: serverName })
|
|
40
118
|
});
|
|
@@ -48,7 +126,10 @@ export const registerDaemonCommand = (program) => {
|
|
|
48
126
|
};
|
|
49
127
|
const startDaemon = (port) => {
|
|
50
128
|
const server = http.createServer(async (req, res) => {
|
|
51
|
-
//
|
|
129
|
+
// Basic Error Handling
|
|
130
|
+
req.on('error', (err) => {
|
|
131
|
+
console.error('[Daemon] Request error:', err);
|
|
132
|
+
});
|
|
52
133
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
53
134
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
54
135
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
@@ -58,8 +139,15 @@ const startDaemon = (port) => {
|
|
|
58
139
|
return;
|
|
59
140
|
}
|
|
60
141
|
if (req.method === 'GET' && req.url === '/status') {
|
|
142
|
+
const initStatus = connectionPool.getInitStatus();
|
|
143
|
+
const connections = await connectionPool.getActiveConnectionDetails(!initStatus.initializing);
|
|
61
144
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
62
|
-
res.end(JSON.stringify({
|
|
145
|
+
res.end(JSON.stringify({
|
|
146
|
+
status: 'running',
|
|
147
|
+
version: pkg.version,
|
|
148
|
+
connections,
|
|
149
|
+
...initStatus
|
|
150
|
+
}));
|
|
63
151
|
return;
|
|
64
152
|
}
|
|
65
153
|
// ... (restart/stop/call handlers) ...
|
|
@@ -121,17 +209,46 @@ const startDaemon = (port) => {
|
|
|
121
209
|
});
|
|
122
210
|
return;
|
|
123
211
|
}
|
|
212
|
+
if (req.method === 'POST' && req.url === '/list') {
|
|
213
|
+
let body = '';
|
|
214
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
215
|
+
req.on('end', async () => {
|
|
216
|
+
try {
|
|
217
|
+
const { server: serverName } = JSON.parse(body);
|
|
218
|
+
if (!serverName) {
|
|
219
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
220
|
+
res.end(JSON.stringify({ error: 'Missing server name' }));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const client = await connectionPool.getClient(serverName);
|
|
224
|
+
const toolsResult = await client.listTools();
|
|
225
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
226
|
+
res.end(JSON.stringify(toolsResult));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error(`[Daemon] Error listing tools:`, error);
|
|
230
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
231
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
124
236
|
res.writeHead(404);
|
|
125
237
|
res.end();
|
|
126
238
|
});
|
|
127
|
-
server.listen(port, () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
239
|
+
server.listen(port, async () => {
|
|
240
|
+
// Initialize all connections eagerly
|
|
241
|
+
await connectionPool.initializeAll();
|
|
242
|
+
});
|
|
243
|
+
server.on('error', (e) => {
|
|
244
|
+
if (e.code === 'EADDRINUSE') {
|
|
245
|
+
console.log(chalk.yellow(`Port ${port} is already in use.`));
|
|
246
|
+
process.exit(0); // Exit gracefully if already running
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.error('[Daemon] Server error:', e);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
135
252
|
});
|
|
136
253
|
const shutdown = async () => {
|
|
137
254
|
console.log('\n[Daemon] Shutting down...');
|
package/dist/commands/server.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { configManager } from '../core/config.js';
|
|
3
3
|
export const registerServerCommands = (program) => {
|
|
4
|
-
const
|
|
5
|
-
.description('Manage MCP servers');
|
|
6
|
-
serverCmd.command('list')
|
|
7
|
-
.alias('ls')
|
|
8
|
-
.description('List all configured servers')
|
|
9
|
-
.action(() => {
|
|
4
|
+
const listServersAction = () => {
|
|
10
5
|
const servers = configManager.listServers();
|
|
11
6
|
if (servers.length === 0) {
|
|
12
7
|
console.log(chalk.yellow('No servers configured.'));
|
|
@@ -25,7 +20,18 @@ export const registerServerCommands = (program) => {
|
|
|
25
20
|
}
|
|
26
21
|
console.log('');
|
|
27
22
|
});
|
|
28
|
-
}
|
|
23
|
+
};
|
|
24
|
+
// Register top-level list command
|
|
25
|
+
program.command('list')
|
|
26
|
+
.alias('ls')
|
|
27
|
+
.description('List all configured servers')
|
|
28
|
+
.action(listServersAction);
|
|
29
|
+
const serverCmd = program.command('server')
|
|
30
|
+
.description('Manage MCP servers');
|
|
31
|
+
serverCmd.command('list')
|
|
32
|
+
.alias('ls')
|
|
33
|
+
.description('List all configured servers')
|
|
34
|
+
.action(listServersAction);
|
|
29
35
|
serverCmd.command('add <name>')
|
|
30
36
|
.description('Add a new MCP server')
|
|
31
37
|
.option('--type <type>', 'Server type (stdio, sse, or http)', 'stdio')
|
|
@@ -86,12 +92,15 @@ export const registerServerCommands = (program) => {
|
|
|
86
92
|
serverCmd.command('update <name>')
|
|
87
93
|
.description('Update a server')
|
|
88
94
|
.option('--command <command>', 'New command')
|
|
95
|
+
.option('--args [args...]', 'New arguments for the command')
|
|
89
96
|
.option('--url <url>', 'New URL')
|
|
90
97
|
.action((name, options) => {
|
|
91
98
|
try {
|
|
92
99
|
const updates = {};
|
|
93
100
|
if (options.command)
|
|
94
101
|
updates.command = options.command;
|
|
102
|
+
if (options.args)
|
|
103
|
+
updates.args = options.args;
|
|
95
104
|
if (options.url)
|
|
96
105
|
updates.url = options.url;
|
|
97
106
|
if (Object.keys(updates).length === 0) {
|
package/dist/commands/tools.js
CHANGED
|
@@ -1,49 +1,51 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { configManager } from '../core/config.js';
|
|
3
|
-
import {
|
|
3
|
+
import { DaemonClient } from '../core/daemon-client.js';
|
|
4
|
+
function printTools(serverName, tools) {
|
|
5
|
+
console.log(chalk.bold(`\nAvailable Tools for ${serverName}:`));
|
|
6
|
+
if (!tools || tools.length === 0) {
|
|
7
|
+
console.log(chalk.yellow('No tools found.'));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
tools.forEach((tool) => {
|
|
11
|
+
console.log(chalk.cyan(`\n- ${tool.name}`));
|
|
12
|
+
if (tool.description) {
|
|
13
|
+
console.log(` ${tool.description}`);
|
|
14
|
+
}
|
|
15
|
+
console.log(chalk.gray(' Arguments:'));
|
|
16
|
+
const schema = tool.inputSchema;
|
|
17
|
+
if (schema.properties) {
|
|
18
|
+
Object.entries(schema.properties).forEach(([key, value]) => {
|
|
19
|
+
const required = schema.required?.includes(key) ? chalk.red('*') : '';
|
|
20
|
+
console.log(` ${key}${required}: ${value.type || 'any'} ${value.description ? `(${value.description})` : ''}`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(' None');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
4
29
|
export const registerToolsCommand = (program) => {
|
|
5
30
|
program.command('tools <server>')
|
|
6
31
|
.description('List available tools on a server')
|
|
7
32
|
.action(async (serverName) => {
|
|
33
|
+
// Check if server exists in config first
|
|
8
34
|
const serverConfig = configManager.getServer(serverName);
|
|
9
35
|
if (!serverConfig) {
|
|
10
|
-
console.error(chalk.red(`Server "${serverName}" not found.`));
|
|
36
|
+
console.error(chalk.red(`Server "${serverName}" not found in config.`));
|
|
11
37
|
process.exit(1);
|
|
12
38
|
}
|
|
13
|
-
const client = new McpClientService();
|
|
14
39
|
try {
|
|
15
|
-
//
|
|
16
|
-
await
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.log(chalk.yellow('No tools found.'));
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
tools.tools.forEach(tool => {
|
|
24
|
-
console.log(chalk.cyan(`\n- ${tool.name}`));
|
|
25
|
-
if (tool.description) {
|
|
26
|
-
console.log(` ${tool.description}`);
|
|
27
|
-
}
|
|
28
|
-
console.log(chalk.gray(' Arguments:'));
|
|
29
|
-
const schema = tool.inputSchema;
|
|
30
|
-
if (schema.properties) {
|
|
31
|
-
Object.entries(schema.properties).forEach(([key, value]) => {
|
|
32
|
-
const required = schema.required?.includes(key) ? chalk.red('*') : '';
|
|
33
|
-
console.log(` ${key}${required}: ${value.type || 'any'} ${value.description ? `(${value.description})` : ''}`);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
console.log(' None');
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}
|
|
40
|
+
// Auto-start daemon if needed
|
|
41
|
+
await DaemonClient.ensureDaemon();
|
|
42
|
+
// List via daemon
|
|
43
|
+
const tools = await DaemonClient.listTools(serverName);
|
|
44
|
+
printTools(serverName, tools);
|
|
41
45
|
}
|
|
42
46
|
catch (error) {
|
|
43
47
|
console.error(chalk.red(`Failed to list tools: ${error.message}`));
|
|
44
|
-
|
|
45
|
-
finally {
|
|
46
|
-
await client.close();
|
|
48
|
+
process.exit(1);
|
|
47
49
|
}
|
|
48
50
|
});
|
|
49
51
|
};
|
package/dist/core/client.js
CHANGED
|
@@ -6,13 +6,39 @@ import { EventSource } from 'eventsource';
|
|
|
6
6
|
// Required for SSEClientTransport in Node.js environment
|
|
7
7
|
// @ts-ignore
|
|
8
8
|
global.EventSource = EventSource;
|
|
9
|
+
const resolveEnvPlaceholders = (input) => {
|
|
10
|
+
const missing = new Set();
|
|
11
|
+
const resolved = input.replace(/\$\{([A-Za-z0-9_]+)\}|\$([A-Za-z0-9_]+)/g, (match, braced, bare) => {
|
|
12
|
+
const key = braced || bare;
|
|
13
|
+
const val = process.env[key];
|
|
14
|
+
if (val === undefined) {
|
|
15
|
+
missing.add(key);
|
|
16
|
+
return match;
|
|
17
|
+
}
|
|
18
|
+
return val;
|
|
19
|
+
});
|
|
20
|
+
if (missing.size > 0) {
|
|
21
|
+
const list = Array.from(missing).join(', ');
|
|
22
|
+
throw new Error(`Missing environment variables: ${list}`);
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
};
|
|
9
26
|
export class McpClientService {
|
|
10
27
|
client = null;
|
|
11
28
|
transport = null;
|
|
12
29
|
async connect(config) {
|
|
13
30
|
try {
|
|
14
31
|
if (config.type === 'stdio') {
|
|
15
|
-
const
|
|
32
|
+
const resolvedConfigEnv = {};
|
|
33
|
+
if (config.env) {
|
|
34
|
+
for (const key in config.env) {
|
|
35
|
+
const val = config.env[key];
|
|
36
|
+
if (typeof val === 'string') {
|
|
37
|
+
resolvedConfigEnv[key] = resolveEnvPlaceholders(val);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const rawEnv = config.env ? { ...process.env, ...resolvedConfigEnv } : process.env;
|
|
16
42
|
const env = {};
|
|
17
43
|
for (const key in rawEnv) {
|
|
18
44
|
const val = rawEnv[key];
|
|
@@ -20,17 +46,20 @@ export class McpClientService {
|
|
|
20
46
|
env[key] = val;
|
|
21
47
|
}
|
|
22
48
|
}
|
|
49
|
+
const args = config.args ? config.args.map(arg => resolveEnvPlaceholders(arg)) : [];
|
|
23
50
|
this.transport = new StdioClientTransport({
|
|
24
51
|
command: config.command,
|
|
25
|
-
args
|
|
52
|
+
args,
|
|
26
53
|
env: env,
|
|
27
54
|
});
|
|
28
55
|
}
|
|
29
56
|
else if (config.type === 'http') {
|
|
30
|
-
|
|
57
|
+
const url = resolveEnvPlaceholders(config.url);
|
|
58
|
+
this.transport = new StreamableHTTPClientTransport(new URL(url));
|
|
31
59
|
}
|
|
32
60
|
else {
|
|
33
|
-
|
|
61
|
+
const url = resolveEnvPlaceholders(config.url);
|
|
62
|
+
this.transport = new SSEClientTransport(new URL(url));
|
|
34
63
|
}
|
|
35
64
|
this.client = new Client({
|
|
36
65
|
name: 'mcp-cli',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { DAEMON_BASE_URL } from './constants.js';
|
|
4
|
+
export class DaemonClient {
|
|
5
|
+
static async isRunning() {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(`${DAEMON_BASE_URL}/status`);
|
|
8
|
+
return res.ok;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
static async startDaemon() {
|
|
15
|
+
console.log(chalk.gray('Starting background daemon...'));
|
|
16
|
+
// Use process.argv[1] which points to the CLI entry point
|
|
17
|
+
// detached: true allows the child to keep running after parent exits
|
|
18
|
+
const subprocess = spawn(process.execPath, [process.argv[1], 'daemon', 'start'], {
|
|
19
|
+
detached: true,
|
|
20
|
+
stdio: 'ignore',
|
|
21
|
+
env: process.env
|
|
22
|
+
});
|
|
23
|
+
subprocess.unref();
|
|
24
|
+
// Wait for daemon to be ready
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
while (Date.now() - start < 5000) { // 5s timeout
|
|
27
|
+
if (await this.isRunning()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
31
|
+
}
|
|
32
|
+
throw new Error('Daemon failed to start within timeout');
|
|
33
|
+
}
|
|
34
|
+
static async ensureDaemon() {
|
|
35
|
+
if (await this.isRunning()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await this.startDaemon();
|
|
39
|
+
}
|
|
40
|
+
static async executeTool(serverName, toolName, args) {
|
|
41
|
+
const response = await fetch(`${DAEMON_BASE_URL}/call`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ server: serverName, tool: toolName, args }),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const err = await response.json();
|
|
48
|
+
throw new Error(err.error || 'Daemon error');
|
|
49
|
+
}
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
return data.result;
|
|
52
|
+
}
|
|
53
|
+
static async listTools(serverName) {
|
|
54
|
+
const response = await fetch(`${DAEMON_BASE_URL}/list`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ server: serverName }),
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const err = await response.json();
|
|
61
|
+
throw new Error(err.error || 'Daemon error');
|
|
62
|
+
}
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
// The daemon returns { tools: { tools: [...] } } because of ListToolsResult wrapping
|
|
65
|
+
return data.tools.tools || [];
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/core/pool.js
CHANGED
|
@@ -2,7 +2,9 @@ import { McpClientService } from './client.js';
|
|
|
2
2
|
import { configManager } from './config.js';
|
|
3
3
|
export class ConnectionPool {
|
|
4
4
|
clients = new Map();
|
|
5
|
-
|
|
5
|
+
initializing = false;
|
|
6
|
+
initialized = false;
|
|
7
|
+
async getClient(serverName, options) {
|
|
6
8
|
if (this.clients.has(serverName)) {
|
|
7
9
|
return this.clients.get(serverName);
|
|
8
10
|
}
|
|
@@ -10,9 +12,17 @@ export class ConnectionPool {
|
|
|
10
12
|
if (!serverConfig) {
|
|
11
13
|
throw new Error(`Server "${serverName}" not found in config.`);
|
|
12
14
|
}
|
|
13
|
-
console.log(`[Daemon] Connecting to server: ${serverName}...`);
|
|
14
15
|
const client = new McpClientService();
|
|
15
|
-
|
|
16
|
+
const connectPromise = client.connect(serverConfig);
|
|
17
|
+
if (options?.timeoutMs) {
|
|
18
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
19
|
+
setTimeout(() => reject(new Error(`Connection timeout after ${options.timeoutMs}ms`)), options.timeoutMs);
|
|
20
|
+
});
|
|
21
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await connectPromise;
|
|
25
|
+
}
|
|
16
26
|
this.clients.set(serverName, client);
|
|
17
27
|
return client;
|
|
18
28
|
}
|
|
@@ -42,5 +52,59 @@ export class ConnectionPool {
|
|
|
42
52
|
}
|
|
43
53
|
this.clients.clear();
|
|
44
54
|
}
|
|
55
|
+
async initializeAll() {
|
|
56
|
+
const servers = configManager.listServers();
|
|
57
|
+
this.initializing = true;
|
|
58
|
+
this.initialized = false;
|
|
59
|
+
// 过滤掉 disabled 的服务器
|
|
60
|
+
const enabledServers = servers.filter(server => {
|
|
61
|
+
const disabled = server.disabled === true;
|
|
62
|
+
if (disabled) {
|
|
63
|
+
console.log(`[Daemon] Skipping disabled server: ${server.name}`);
|
|
64
|
+
}
|
|
65
|
+
return !disabled;
|
|
66
|
+
});
|
|
67
|
+
if (enabledServers.length === 0) {
|
|
68
|
+
console.log('[Daemon] No enabled servers to initialize.');
|
|
69
|
+
this.initializing = false;
|
|
70
|
+
this.initialized = true;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`[Daemon] Initializing ${enabledServers.length} connection(s)...`);
|
|
74
|
+
for (const server of enabledServers) {
|
|
75
|
+
try {
|
|
76
|
+
console.log(`[Daemon] Connecting to server: ${server.name}...`);
|
|
77
|
+
await this.getClient(server.name, { timeoutMs: 8000 });
|
|
78
|
+
console.log(`[Daemon] ✓ Connected to ${server.name}`);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error(`[Daemon] ✗ Failed to connect to ${server.name}:`, error.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.initializing = false;
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
console.log('[Daemon] Initialization complete.');
|
|
87
|
+
}
|
|
88
|
+
getInitStatus() {
|
|
89
|
+
return { initializing: this.initializing, initialized: this.initialized };
|
|
90
|
+
}
|
|
91
|
+
async getActiveConnectionDetails(includeTools = true) {
|
|
92
|
+
const details = [];
|
|
93
|
+
for (const [name, client] of this.clients) {
|
|
94
|
+
let toolsCount = null;
|
|
95
|
+
let status = 'connected';
|
|
96
|
+
if (includeTools) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.listTools();
|
|
99
|
+
toolsCount = result.tools.length;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
status = 'error';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
details.push({ name, toolsCount, status });
|
|
106
|
+
}
|
|
107
|
+
return details;
|
|
108
|
+
}
|
|
45
109
|
}
|
|
46
110
|
export const connectionPool = new ConnectionPool();
|