@maplezzk/mcps 1.0.4 → 1.0.8

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,36 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { configManager } from '../core/config.js';
3
- import { McpClientService } from '../core/client.js';
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
- // 1. Try Daemon first
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
- await client.connect(serverConfig);
104
- const result = await client.callTool(toolName, params);
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(`Tool call failed: ${error.message}`));
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
  };
@@ -2,39 +2,58 @@ import http from 'http';
2
2
  import chalk from 'chalk';
3
3
  import { connectionPool } from '../core/pool.js';
4
4
  import { createRequire } from 'module';
5
+ import { DAEMON_PORT } from '../core/constants.js';
5
6
  const require = createRequire(import.meta.url);
6
7
  const pkg = require('../../package.json');
7
- const PORT = 4100;
8
8
  export const registerDaemonCommand = (program) => {
9
9
  const daemonCmd = program.command('daemon')
10
10
  .description('Manage the mcps daemon');
11
11
  daemonCmd.command('start', { isDefault: true })
12
12
  .description('Start the daemon (default)')
13
- .option('-p, --port <number>', 'Port to listen on', String(PORT))
13
+ .option('-p, --port <number>', 'Port to listen on', String(DAEMON_PORT))
14
14
  .action(async (options) => {
15
15
  const port = parseInt(options.port);
16
- // ... (server logic) ...
17
- // Need to move the server creation logic here or keep it in the main action if no subcommands match?
18
- // Commander handling of default commands with subcommands can be tricky.
19
- // Let's refactor to use separate actions.
20
16
  startDaemon(port);
21
17
  });
22
18
  daemonCmd.command('stop')
23
19
  .description('Stop the running daemon')
24
20
  .action(async () => {
25
21
  try {
26
- await fetch(`http://localhost:${PORT}/stop`, { method: 'POST' });
22
+ await fetch(`http://localhost:${DAEMON_PORT}/stop`, { method: 'POST' });
27
23
  console.log(chalk.green('Daemon stopped successfully.'));
28
24
  }
29
25
  catch (e) {
30
26
  console.error(chalk.red('Failed to stop daemon. Is it running?'));
31
27
  }
32
28
  });
29
+ daemonCmd.command('status')
30
+ .description('Check daemon status')
31
+ .action(async () => {
32
+ try {
33
+ const res = await fetch(`http://localhost:${DAEMON_PORT}/status`);
34
+ const data = await res.json();
35
+ console.log(chalk.green(`Daemon is running (v${data.version})`));
36
+ if (data.connections && data.connections.length > 0) {
37
+ console.log(chalk.bold('\nActive Connections:'));
38
+ data.connections.forEach((conn) => {
39
+ const count = conn.toolsCount !== null ? `(${conn.toolsCount} tools)` : '(error listing tools)';
40
+ const status = conn.status === 'error' ? chalk.red('[Error]') : '';
41
+ console.log(chalk.cyan(`- ${conn.name} ${chalk.gray(count)} ${status}`));
42
+ });
43
+ }
44
+ else {
45
+ console.log(chalk.gray('No active connections.'));
46
+ }
47
+ }
48
+ catch (e) {
49
+ console.error(chalk.red('Daemon is not running.'));
50
+ }
51
+ });
33
52
  daemonCmd.command('restart [server]')
34
53
  .description('Restart the daemon or a specific server connection')
35
54
  .action(async (serverName) => {
36
55
  try {
37
- const res = await fetch(`http://localhost:${PORT}/restart`, {
56
+ const res = await fetch(`http://localhost:${DAEMON_PORT}/restart`, {
38
57
  method: 'POST',
39
58
  body: JSON.stringify({ server: serverName })
40
59
  });
@@ -58,8 +77,13 @@ const startDaemon = (port) => {
58
77
  return;
59
78
  }
60
79
  if (req.method === 'GET' && req.url === '/status') {
80
+ const connections = await connectionPool.getActiveConnectionDetails();
61
81
  res.writeHead(200, { 'Content-Type': 'application/json' });
62
- res.end(JSON.stringify({ status: 'running', version: pkg.version }));
82
+ res.end(JSON.stringify({
83
+ status: 'running',
84
+ version: pkg.version,
85
+ connections
86
+ }));
63
87
  return;
64
88
  }
65
89
  // ... (restart/stop/call handlers) ...
@@ -121,6 +145,30 @@ const startDaemon = (port) => {
121
145
  });
122
146
  return;
123
147
  }
148
+ if (req.method === 'POST' && req.url === '/list') {
149
+ let body = '';
150
+ req.on('data', chunk => { body += chunk.toString(); });
151
+ req.on('end', async () => {
152
+ try {
153
+ const { server: serverName } = JSON.parse(body);
154
+ if (!serverName) {
155
+ res.writeHead(400, { 'Content-Type': 'application/json' });
156
+ res.end(JSON.stringify({ error: 'Missing server name' }));
157
+ return;
158
+ }
159
+ const client = await connectionPool.getClient(serverName);
160
+ const tools = await client.listTools();
161
+ res.writeHead(200, { 'Content-Type': 'application/json' });
162
+ res.end(JSON.stringify({ tools }));
163
+ }
164
+ catch (error) {
165
+ console.error(`[Daemon] Error listing tools:`, error);
166
+ res.writeHead(500, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({ error: error.message }));
168
+ }
169
+ });
170
+ return;
171
+ }
124
172
  res.writeHead(404);
125
173
  res.end();
126
174
  });
@@ -1,49 +1,51 @@
1
1
  import chalk from 'chalk';
2
2
  import { configManager } from '../core/config.js';
3
- import { McpClientService } from '../core/client.js';
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
- // console.log(chalk.gray(`Connecting to ${serverName}...`));
16
- await client.connect(serverConfig);
17
- const tools = await client.listTools();
18
- console.log(chalk.bold(`\nAvailable Tools for ${serverName}:`));
19
- if (tools.tools.length === 0) {
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
  };
@@ -0,0 +1,2 @@
1
+ export const DAEMON_PORT = 4100;
2
+ export const DAEMON_BASE_URL = `http://localhost:${DAEMON_PORT}`;
@@ -0,0 +1,66 @@
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
+ return data.tools;
65
+ }
66
+ }
package/dist/core/pool.js CHANGED
@@ -42,5 +42,21 @@ export class ConnectionPool {
42
42
  }
43
43
  this.clients.clear();
44
44
  }
45
+ async getActiveConnectionDetails() {
46
+ const details = [];
47
+ for (const [name, client] of this.clients) {
48
+ let toolsCount = null;
49
+ let status = 'connected';
50
+ try {
51
+ const result = await client.listTools();
52
+ toolsCount = result.tools.length;
53
+ }
54
+ catch (e) {
55
+ status = 'error';
56
+ }
57
+ details.push({ name, toolsCount, status });
58
+ }
59
+ return details;
60
+ }
45
61
  }
46
62
  export const connectionPool = new ConnectionPool();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.4",
3
+ "version": "1.0.8",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"