@maplezzk/mcps 1.0.31 → 1.1.1

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,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { configManager } from '../core/config.js';
3
3
  import { DaemonClient } from '../core/daemon-client.js';
4
+ import { detectServerType } from '../types/config.js';
4
5
  export const registerServerCommands = (program) => {
5
6
  const listServersAction = () => {
6
7
  const servers = configManager.listServers();
@@ -30,19 +31,21 @@ export const registerServerCommands = (program) => {
30
31
  // Build table rows
31
32
  const rows = servers.map(server => {
32
33
  const disabled = server.disabled === true;
33
- const typeColor = server.type === 'stdio' ? chalk.cyan : chalk.yellow;
34
+ const serverType = detectServerType(server);
35
+ const typeColor = serverType === 'stdio' ? chalk.cyan : chalk.yellow;
34
36
  const enabledMark = disabled ? chalk.red('✗') : chalk.green('✓');
35
37
  // Build command/URL string
36
38
  let command = '';
37
- if (server.type === 'stdio') {
38
- command = `${server.command} ${server.args.join(' ')}`;
39
+ if ('command' in server && server.command) {
40
+ const args = server.args;
41
+ command = `${server.command} ${args?.join(' ') || ''}`;
39
42
  }
40
- else {
41
- command = server.url || '';
43
+ else if ('url' in server && server.url) {
44
+ command = server.url;
42
45
  }
43
46
  return {
44
47
  name: server.name,
45
- type: typeColor(server.type),
48
+ type: typeColor(serverType),
46
49
  enabled: enabledMark,
47
50
  command: command,
48
51
  disabled
@@ -67,12 +70,10 @@ export const registerServerCommands = (program) => {
67
70
  };
68
71
  const addServerAction = (name, options) => {
69
72
  try {
70
- if (options.type === 'sse' || options.type === 'http') {
73
+ if (options.type === 'sse' || options.type === 'http' || options.url) {
71
74
  if (!options.url)
72
- throw new Error(`URL is required for ${options.type} servers`);
73
- configManager.addServer({
74
- name,
75
- type: options.type,
75
+ throw new Error(`URL is required for ${options.type || 'HTTP/SSE'} servers`);
76
+ configManager.addServer(name, {
76
77
  url: options.url,
77
78
  });
78
79
  }
@@ -89,9 +90,7 @@ export const registerServerCommands = (program) => {
89
90
  env[k] = v;
90
91
  });
91
92
  }
92
- configManager.addServer({
93
- name,
94
- type: 'stdio',
93
+ configManager.addServer(name, {
95
94
  command: options.command,
96
95
  args: options.args || [],
97
96
  env: Object.keys(env).length > 0 ? env : undefined,
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
3
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
+ import { detectServerType } from '../types/config.js';
5
6
  import { EventSource } from 'eventsource';
6
7
  import { exec } from 'child_process';
7
8
  import { promisify } from 'util';
@@ -59,23 +60,25 @@ export class McpClientService {
59
60
  static globalPidsBeforeConnection = new Set();
60
61
  async connect(config, serverName = '') {
61
62
  this.serverName = serverName;
62
- this.serverType = config.type;
63
+ const serverType = detectServerType(config);
64
+ this.serverType = serverType;
63
65
  this.daemonPid = process.pid;
64
66
  try {
65
- if (config.type === 'stdio') {
67
+ if (serverType === 'stdio' && 'command' in config) {
68
+ const stdioConfig = config;
66
69
  // 保存命令和参数用于后续清理进程
67
- this.serverCommand = config.command;
68
- this.serverArgs = config.args || [];
70
+ this.serverCommand = stdioConfig.command;
71
+ this.serverArgs = stdioConfig.args || [];
69
72
  const resolvedConfigEnv = {};
70
- if (config.env) {
71
- for (const key in config.env) {
72
- const val = config.env[key];
73
+ if (stdioConfig.env) {
74
+ for (const key in stdioConfig.env) {
75
+ const val = stdioConfig.env[key];
73
76
  if (typeof val === 'string') {
74
77
  resolvedConfigEnv[key] = resolveEnvPlaceholders(val);
75
78
  }
76
79
  }
77
80
  }
78
- const rawEnv = config.env ? { ...process.env, ...resolvedConfigEnv } : process.env;
81
+ const rawEnv = stdioConfig.env ? { ...process.env, ...resolvedConfigEnv } : process.env;
79
82
  const env = {};
80
83
  for (const key in rawEnv) {
81
84
  const val = rawEnv[key];
@@ -83,21 +86,24 @@ export class McpClientService {
83
86
  env[key] = val;
84
87
  }
85
88
  }
86
- const args = config.args ? config.args.map(arg => resolveEnvPlaceholders(arg)) : [];
89
+ const args = stdioConfig.args ? stdioConfig.args.map(arg => resolveEnvPlaceholders(arg)) : [];
87
90
  this.transport = new StdioClientTransport({
88
- command: config.command,
91
+ command: stdioConfig.command,
89
92
  args,
90
93
  env: env,
91
94
  });
92
95
  }
93
- else if (config.type === 'http') {
96
+ else if (serverType === 'http' && 'url' in config) {
94
97
  const url = resolveEnvPlaceholders(config.url);
95
98
  this.transport = new StreamableHTTPClientTransport(new URL(url));
96
99
  }
97
- else {
100
+ else if ('url' in config) {
98
101
  const url = resolveEnvPlaceholders(config.url);
99
102
  this.transport = new SSEClientTransport(new URL(url));
100
103
  }
104
+ else {
105
+ throw new Error('Invalid server configuration: must have either command (for stdio) or url (for sse/http)');
106
+ }
101
107
  this.client = new Client({
102
108
  name: 'mcp-cli',
103
109
  version: '1.0.0',
@@ -18,48 +18,36 @@ export class ConfigManager {
18
18
  loadConfig() {
19
19
  this.ensureConfigDir();
20
20
  if (!fs.existsSync(this.configFile)) {
21
- return { servers: [] };
21
+ return { mcpServers: {} };
22
22
  }
23
23
  try {
24
24
  const content = fs.readFileSync(this.configFile, 'utf-8');
25
25
  const json = JSON.parse(content);
26
- // Log for debugging (can be removed later or controlled by verbose flag)
27
- // console.log('Loading config from:', this.configFile);
28
26
  if (!json || typeof json !== 'object') {
29
27
  console.warn('Invalid config file structure. Expected JSON object.');
30
- return { servers: [] };
28
+ return { mcpServers: {} };
31
29
  }
32
- // Handle both { servers: [...] } and { mcpServers: { ... } } (VSCode/Claude style)
33
- let servers = [];
34
- if (Array.isArray(json.servers)) {
35
- servers = json.servers;
30
+ // Only accept standard MCP format: { mcpServers: { ... } }
31
+ if (!json.mcpServers || typeof json.mcpServers !== 'object') {
32
+ console.warn('Invalid config format. Expected { mcpServers: { ... } }');
33
+ return { mcpServers: {} };
36
34
  }
37
- else if (json.mcpServers && typeof json.mcpServers === 'object') {
38
- // Convert map to array and add default type='stdio' if missing
39
- servers = Object.entries(json.mcpServers).map(([name, config]) => ({
40
- name,
41
- type: config.type || (config.command ? 'stdio' : undefined), // Auto-detect type
42
- ...config
43
- }));
44
- }
45
- const validServers = [];
46
- for (const server of servers) {
47
- const result = ServerConfigSchema.safeParse(server);
35
+ // Validate each server config
36
+ const validServers = {};
37
+ for (const [name, serverConfig] of Object.entries(json.mcpServers)) {
38
+ const result = ServerConfigSchema.safeParse(serverConfig);
48
39
  if (result.success) {
49
- validServers.push(result.data);
40
+ validServers[name] = result.data;
50
41
  }
51
42
  else {
52
- // Only warn if it looks like a server config we *should* have supported
53
- if (server.name) {
54
- console.warn(`Skipping invalid server config "${server.name}":`, result.error.errors[0]?.message);
55
- }
43
+ console.warn(`Skipping invalid server config "${name}":`, result.error.errors[0]?.message);
56
44
  }
57
45
  }
58
- return { servers: validServers };
46
+ return { mcpServers: validServers };
59
47
  }
60
48
  catch (error) {
61
49
  console.error('Failed to parse config file:', error);
62
- return { servers: [] };
50
+ return { mcpServers: {} };
63
51
  }
64
52
  }
65
53
  saveConfig(config) {
@@ -67,43 +55,47 @@ export class ConfigManager {
67
55
  fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2), 'utf-8');
68
56
  }
69
57
  listServers() {
70
- return this.loadConfig().servers;
58
+ const config = this.loadConfig();
59
+ return Object.entries(config.mcpServers).map(([name, server]) => ({
60
+ name,
61
+ ...server,
62
+ }));
71
63
  }
72
64
  getServer(name) {
73
65
  const config = this.loadConfig();
74
- return config.servers.find(s => s.name === name);
66
+ const server = config.mcpServers[name];
67
+ if (!server)
68
+ return undefined;
69
+ return { name, ...server };
75
70
  }
76
- addServer(server) {
71
+ addServer(name, server) {
77
72
  const config = this.loadConfig();
78
- if (config.servers.find(s => s.name === server.name)) {
79
- throw new Error(`Server with name "${server.name}" already exists.`);
73
+ if (config.mcpServers[name]) {
74
+ throw new Error(`Server with name "${name}" already exists.`);
80
75
  }
81
- config.servers.push(server);
76
+ config.mcpServers[name] = server;
82
77
  this.saveConfig(config);
83
78
  }
84
79
  removeServer(name) {
85
80
  const config = this.loadConfig();
86
- const initialLength = config.servers.length;
87
- config.servers = config.servers.filter(s => s.name !== name);
88
- if (config.servers.length === initialLength) {
81
+ if (!config.mcpServers[name]) {
89
82
  throw new Error(`Server with name "${name}" not found.`);
90
83
  }
84
+ delete config.mcpServers[name];
91
85
  this.saveConfig(config);
92
86
  }
93
87
  updateServer(name, updates) {
94
88
  const config = this.loadConfig();
95
- const index = config.servers.findIndex(s => s.name === name);
96
- if (index === -1) {
89
+ const current = config.mcpServers[name];
90
+ if (!current) {
97
91
  throw new Error(`Server with name "${name}" not found.`);
98
92
  }
99
- const current = config.servers[index];
100
93
  const updated = { ...current, ...updates };
101
- // Validate the updated object matches the schema (especially type consistency)
102
94
  const result = ServerConfigSchema.safeParse(updated);
103
95
  if (!result.success) {
104
96
  throw new Error(`Invalid update: ${result.error.message}`);
105
97
  }
106
- config.servers[index] = result.data;
98
+ config.mcpServers[name] = result.data;
107
99
  this.saveConfig(config);
108
100
  }
109
101
  }
@@ -26,74 +26,75 @@ describe('ConfigManager', () => {
26
26
  });
27
27
  describe('addServer', () => {
28
28
  it('should add a stdio server', () => {
29
- const server = {
30
- name: 'test-stdio',
31
- type: 'stdio',
29
+ const serverName = 'test-stdio';
30
+ const serverConfig = {
32
31
  command: 'node',
33
32
  args: ['--version']
34
33
  };
35
- manager.addServer(server);
34
+ manager.addServer(serverName, serverConfig);
36
35
  const servers = manager.listServers();
37
36
  expect(servers).toHaveLength(1);
38
- expect(servers[0]).toMatchObject(server);
37
+ expect(servers[0].name).toBe(serverName);
38
+ expect(servers[0].command).toBe('node');
39
+ expect(servers[0].args).toEqual(['--version']);
39
40
  });
40
41
  it('should add an sse server', () => {
41
- const server = {
42
- name: 'test-sse',
43
- type: 'sse',
42
+ const serverName = 'test-sse';
43
+ const serverConfig = {
44
44
  url: 'http://localhost:3000/sse'
45
45
  };
46
- manager.addServer(server);
46
+ manager.addServer(serverName, serverConfig);
47
47
  const retrieved = manager.getServer('test-sse');
48
- expect(retrieved).toMatchObject(server);
48
+ expect(retrieved).toBeDefined();
49
+ expect(retrieved.name).toBe(serverName);
50
+ expect(retrieved.url).toBe('http://localhost:3000/sse');
49
51
  });
50
52
  it('should add an http server', () => {
51
- const server = {
52
- name: 'test-http',
53
- type: 'http',
53
+ const serverName = 'test-http';
54
+ const serverConfig = {
54
55
  url: 'http://localhost:3000/mcp'
55
56
  };
56
- manager.addServer(server);
57
+ manager.addServer(serverName, serverConfig);
57
58
  const retrieved = manager.getServer('test-http');
58
- expect(retrieved).toMatchObject(server);
59
+ expect(retrieved).toBeDefined();
60
+ expect(retrieved.name).toBe(serverName);
61
+ expect(retrieved.url).toBe('http://localhost:3000/mcp');
59
62
  });
60
63
  it('should add server with disabled flag', () => {
61
- const server = {
62
- name: 'disabled-server',
63
- type: 'stdio',
64
+ const serverName = 'disabled-server';
65
+ const serverConfig = {
64
66
  command: 'node',
65
67
  args: [],
66
68
  disabled: true
67
69
  };
68
- manager.addServer(server);
70
+ manager.addServer(serverName, serverConfig);
69
71
  const retrieved = manager.getServer('disabled-server');
70
- expect(retrieved).toMatchObject({
71
- name: 'disabled-server',
72
- disabled: true
73
- });
72
+ expect(retrieved).toBeDefined();
73
+ expect(retrieved.name).toBe('disabled-server');
74
+ expect(retrieved.disabled).toBe(true);
74
75
  });
75
76
  it('should throw error when adding duplicate server', () => {
76
- const server = {
77
- name: 'duplicate',
78
- type: 'stdio',
77
+ const serverName = 'duplicate';
78
+ const serverConfig = {
79
79
  command: 'node',
80
80
  args: []
81
81
  };
82
- manager.addServer(server);
83
- expect(() => manager.addServer(server)).toThrow('already exists');
82
+ manager.addServer(serverName, serverConfig);
83
+ expect(() => manager.addServer(serverName, serverConfig)).toThrow('already exists');
84
84
  });
85
85
  });
86
86
  describe('getServer', () => {
87
87
  it('should retrieve existing server', () => {
88
- const server = {
89
- name: 'to-retrieve',
90
- type: 'stdio',
88
+ const serverName = 'to-retrieve';
89
+ const serverConfig = {
91
90
  command: 'node',
92
91
  args: []
93
92
  };
94
- manager.addServer(server);
93
+ manager.addServer(serverName, serverConfig);
95
94
  const retrieved = manager.getServer('to-retrieve');
96
- expect(retrieved).toMatchObject(server);
95
+ expect(retrieved).toBeDefined();
96
+ expect(retrieved.name).toBe(serverName);
97
+ expect(retrieved.command).toBe('node');
97
98
  });
98
99
  it('should return undefined for non-existing server', () => {
99
100
  const retrieved = manager.getServer('non-existing');
@@ -106,34 +107,25 @@ describe('ConfigManager', () => {
106
107
  expect(servers).toEqual([]);
107
108
  });
108
109
  it('should return all servers including disabled ones', () => {
109
- const server1 = {
110
- name: 'server-1',
111
- type: 'stdio',
110
+ manager.addServer('server-1', {
112
111
  command: 'node',
113
112
  args: []
114
- };
115
- const server2 = {
116
- name: 'server-2',
117
- type: 'stdio',
113
+ });
114
+ manager.addServer('server-2', {
118
115
  command: 'npm',
119
116
  args: ['start'],
120
117
  disabled: true
121
- };
122
- manager.addServer(server1);
123
- manager.addServer(server2);
118
+ });
124
119
  const servers = manager.listServers();
125
120
  expect(servers).toHaveLength(2);
126
121
  });
127
122
  });
128
123
  describe('removeServer', () => {
129
124
  it('should remove existing server', () => {
130
- const server = {
131
- name: 'to-remove',
132
- type: 'stdio',
125
+ manager.addServer('to-remove', {
133
126
  command: 'node',
134
127
  args: []
135
- };
136
- manager.addServer(server);
128
+ });
137
129
  expect(manager.listServers()).toHaveLength(1);
138
130
  manager.removeServer('to-remove');
139
131
  expect(manager.listServers()).toHaveLength(0);
@@ -144,75 +136,83 @@ describe('ConfigManager', () => {
144
136
  });
145
137
  describe('updateServer', () => {
146
138
  it('should update server configuration', () => {
147
- const original = {
148
- name: 'to-update',
149
- type: 'stdio',
139
+ manager.addServer('to-update', {
150
140
  command: 'node',
151
141
  args: ['--version']
152
- };
153
- manager.addServer(original);
154
- const updates = {
155
- command: 'npm',
156
- args: ['start']
157
- };
158
- manager.updateServer('to-update', updates);
159
- const updated = manager.getServer('to-update');
160
- expect(updated).toMatchObject({
161
- name: 'to-update',
142
+ });
143
+ manager.updateServer('to-update', {
162
144
  command: 'npm',
163
145
  args: ['start']
164
146
  });
147
+ const updated = manager.getServer('to-update');
148
+ expect(updated).toBeDefined();
149
+ expect(updated.command).toBe('npm');
150
+ expect(updated.args).toEqual(['start']);
165
151
  });
166
152
  it('should preserve disabled status during update', () => {
167
- const original = {
168
- name: 'update-disabled',
169
- type: 'stdio',
153
+ manager.addServer('update-disabled', {
170
154
  command: 'node',
171
155
  args: [],
172
156
  disabled: true
173
- };
174
- manager.addServer(original);
157
+ });
175
158
  manager.updateServer('update-disabled', { command: 'npm' });
176
159
  const updated = manager.getServer('update-disabled');
177
- expect(updated?.disabled).toBe(true);
178
- expect(updated?.command).toBe('npm');
160
+ expect(updated.disabled).toBe(true);
161
+ expect(updated.command).toBe('npm');
179
162
  });
180
163
  it('should throw error when updating non-existing server', () => {
181
164
  expect(() => manager.updateServer('non-existing', {})).toThrow('not found');
182
165
  });
183
166
  });
184
167
  describe('persistence', () => {
185
- it('should persist configuration to file', () => {
186
- const server = {
187
- name: 'persistent',
188
- type: 'stdio',
168
+ it('should persist configuration to file in standard MCP format', () => {
169
+ manager.addServer('persistent', {
189
170
  command: 'node',
190
171
  args: []
191
- };
192
- manager.addServer(server);
172
+ });
193
173
  const configFile = path.join(testConfigDir, 'mcp.json');
194
174
  expect(fs.existsSync(configFile)).toBe(true);
195
175
  const content = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
196
- expect(content.servers).toHaveLength(1);
197
- expect(content.servers[0].name).toBe('persistent');
176
+ // Standard MCP format uses mcpServers object, not servers array
177
+ expect(content.mcpServers).toBeDefined();
178
+ expect(content.mcpServers.persistent).toBeDefined();
179
+ expect(content.mcpServers.persistent.command).toBe('node');
198
180
  });
199
181
  it('should load existing configuration on instantiation', () => {
200
182
  const configFile = path.join(testConfigDir, 'mcp.json');
183
+ // Standard MCP format
201
184
  const existingConfig = {
185
+ mcpServers: {
186
+ existing: {
187
+ command: 'node',
188
+ args: []
189
+ }
190
+ }
191
+ };
192
+ fs.writeFileSync(configFile, JSON.stringify(existingConfig, null, 2));
193
+ const newManager = new ConfigManager(testConfigDir);
194
+ const servers = newManager.listServers();
195
+ expect(servers).toHaveLength(1);
196
+ expect(servers[0].name).toBe('existing');
197
+ });
198
+ it('should reject old format (servers array)', () => {
199
+ const configFile = path.join(testConfigDir, 'mcp.json');
200
+ // Old format (should be rejected)
201
+ const oldConfig = {
202
202
  servers: [
203
203
  {
204
- name: 'existing',
204
+ name: 'old-server',
205
205
  type: 'stdio',
206
206
  command: 'node',
207
207
  args: []
208
208
  }
209
209
  ]
210
210
  };
211
- fs.writeFileSync(configFile, JSON.stringify(existingConfig, null, 2));
211
+ fs.writeFileSync(configFile, JSON.stringify(oldConfig, null, 2));
212
212
  const newManager = new ConfigManager(testConfigDir);
213
213
  const servers = newManager.listServers();
214
- expect(servers).toHaveLength(1);
215
- expect(servers[0].name).toBe('existing');
214
+ // Old format should be rejected, returning empty list
215
+ expect(servers).toHaveLength(0);
216
216
  });
217
217
  });
218
218
  });
@@ -1,23 +1,32 @@
1
1
  import { z } from 'zod';
2
+ // Standard MCP server configuration (stdio type)
3
+ export const StdioServerConfigSchema = z.object({
4
+ command: z.string(),
5
+ args: z.array(z.string()).optional(),
6
+ env: z.record(z.string()).optional(),
7
+ cwd: z.string().optional(),
8
+ disabled: z.boolean().optional(),
9
+ autoApprove: z.array(z.string()).optional(),
10
+ }).passthrough();
11
+ // Standard MCP server configuration (sse/http type)
12
+ export const HttpServerConfigSchema = z.object({
13
+ url: z.string().url(),
14
+ disabled: z.boolean().optional(),
15
+ autoApprove: z.array(z.string()).optional(),
16
+ }).passthrough();
17
+ // Union of all server config types
2
18
  export const ServerConfigSchema = z.union([
3
- z.object({
4
- name: z.string(),
5
- type: z.literal('stdio').optional().default('stdio'),
6
- command: z.string(),
7
- args: z.array(z.string()).optional().default([]),
8
- env: z.record(z.string()).optional(),
9
- }).passthrough(),
10
- z.object({
11
- name: z.string(),
12
- type: z.literal('sse'),
13
- url: z.string().url(),
14
- }).passthrough(),
15
- z.object({
16
- name: z.string(),
17
- type: z.literal('http'),
18
- url: z.string().url(),
19
- }).passthrough(),
19
+ StdioServerConfigSchema,
20
+ HttpServerConfigSchema,
20
21
  ]);
22
+ // Standard MCP config format (mcpServers)
21
23
  export const ConfigSchema = z.object({
22
- servers: z.array(ServerConfigSchema).default([]),
24
+ mcpServers: z.record(z.string(), ServerConfigSchema),
23
25
  });
26
+ // Helper to detect server type from config
27
+ export function detectServerType(config) {
28
+ if ('url' in config && typeof config.url === 'string') {
29
+ return config.url.includes('/sse') || config.url.endsWith('/sse') ? 'sse' : 'http';
30
+ }
31
+ return 'stdio';
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/mcps",
3
- "version": "1.0.31",
3
+ "version": "1.1.1",
4
4
  "description": "A CLI to manage and use MCP servers",
5
5
  "publishConfig": {
6
6
  "access": "public"