@maplezzk/mcps 1.0.8 → 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.
@@ -1,3 +1,4 @@
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';
@@ -7,13 +8,71 @@ const require = createRequire(import.meta.url);
7
8
  const pkg = require('../../package.json');
8
9
  export const registerDaemonCommand = (program) => {
9
10
  const daemonCmd = program.command('daemon')
10
- .description('Manage the mcps daemon');
11
- daemonCmd.command('start', { isDefault: true })
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
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
- startDaemon(port);
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
+ }
17
76
  });
18
77
  daemonCmd.command('stop')
19
78
  .description('Stop the running daemon')
@@ -36,7 +95,7 @@ export const registerDaemonCommand = (program) => {
36
95
  if (data.connections && data.connections.length > 0) {
37
96
  console.log(chalk.bold('\nActive Connections:'));
38
97
  data.connections.forEach((conn) => {
39
- const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : '(error listing tools)';
98
+ const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : (data.initializing ? '(initializing)' : '(error listing tools)');
40
99
  const status = conn.status === 'error' ? chalk.red('[Error]') : '';
41
100
  console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
42
101
  });
@@ -67,7 +126,10 @@ export const registerDaemonCommand = (program) => {
67
126
  };
68
127
  const startDaemon = (port) => {
69
128
  const server = http.createServer(async (req, res) => {
70
- // ... (middleware) ...
129
+ // Basic Error Handling
130
+ req.on('error', (err) => {
131
+ console.error('[Daemon] Request error:', err);
132
+ });
71
133
  res.setHeader('Access-Control-Allow-Origin', '*');
72
134
  res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
73
135
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -77,12 +139,14 @@ const startDaemon = (port) => {
77
139
  return;
78
140
  }
79
141
  if (req.method === 'GET' && req.url === '/status') {
80
- const connections = await connectionPool.getActiveConnectionDetails();
142
+ const initStatus = connectionPool.getInitStatus();
143
+ const connections = await connectionPool.getActiveConnectionDetails(!initStatus.initializing);
81
144
  res.writeHead(200, { 'Content-Type': 'application/json' });
82
145
  res.end(JSON.stringify({
83
146
  status: 'running',
84
147
  version: pkg.version,
85
- connections
148
+ connections,
149
+ ...initStatus
86
150
  }));
87
151
  return;
88
152
  }
@@ -157,9 +221,9 @@ const startDaemon = (port) => {
157
221
  return;
158
222
  }
159
223
  const client = await connectionPool.getClient(serverName);
160
- const tools = await client.listTools();
224
+ const toolsResult = await client.listTools();
161
225
  res.writeHead(200, { 'Content-Type': 'application/json' });
162
- res.end(JSON.stringify({ tools }));
226
+ res.end(JSON.stringify(toolsResult));
163
227
  }
164
228
  catch (error) {
165
229
  console.error(`[Daemon] Error listing tools:`, error);
@@ -172,14 +236,19 @@ const startDaemon = (port) => {
172
236
  res.writeHead(404);
173
237
  res.end();
174
238
  });
175
- server.listen(port, () => {
176
- console.log(chalk.green(`
177
- 🚀 mcps Daemon started on port ${port}
178
- -----------------------------------
179
- - Keeps connections to MCP servers alive
180
- - Improves performance for frequent tool calls
181
- - Run 'mcps call ...' in another terminal to use it automatically
182
- `));
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
+ }
183
252
  });
184
253
  const shutdown = async () => {
185
254
  console.log('\n[Daemon] Shutting down...');
@@ -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 serverCmd = program.command('server')
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) {
@@ -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 rawEnv = config.env ? { ...process.env, ...config.env } : process.env;
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: config.args,
52
+ args,
26
53
  env: env,
27
54
  });
28
55
  }
29
56
  else if (config.type === 'http') {
30
- this.transport = new StreamableHTTPClientTransport(new URL(config.url));
57
+ const url = resolveEnvPlaceholders(config.url);
58
+ this.transport = new StreamableHTTPClientTransport(new URL(url));
31
59
  }
32
60
  else {
33
- this.transport = new SSEClientTransport(new URL(config.url));
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',
@@ -61,6 +61,7 @@ export class DaemonClient {
61
61
  throw new Error(err.error || 'Daemon error');
62
62
  }
63
63
  const data = await response.json();
64
- return data.tools;
64
+ // The daemon returns { tools: { tools: [...] } } because of ListToolsResult wrapping
65
+ return data.tools.tools || [];
65
66
  }
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
- async getClient(serverName) {
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
- await client.connect(serverConfig);
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,17 +52,55 @@ export class ConnectionPool {
42
52
  }
43
53
  this.clients.clear();
44
54
  }
45
- async getActiveConnectionDetails() {
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) {
46
92
  const details = [];
47
93
  for (const [name, client] of this.clients) {
48
94
  let toolsCount = null;
49
95
  let status = 'connected';
50
- try {
51
- const result = await client.listTools();
52
- toolsCount = result.tools.length;
53
- }
54
- catch (e) {
55
- status = 'error';
96
+ if (includeTools) {
97
+ try {
98
+ const result = await client.listTools();
99
+ toolsCount = result.tools.length;
100
+ }
101
+ catch (e) {
102
+ status = 'error';
103
+ }
56
104
  }
57
105
  details.push({ name, toolsCount, status });
58
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.8",
3
+ "version": "1.0.14",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"