mcp-config-manager 2.1.0 → 2.3.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/README.md +206 -0
- package/package.json +1 -1
- package/public/index.html +98 -2
- package/public/js/clientView.js +437 -3
- package/public/js/main.js +2 -2
- package/public/js/skillsApi.js +135 -0
- package/public/style.css +877 -26
- package/src/commands-manager.js +268 -0
- package/src/config-manager.js +3 -5
- package/src/server.js +300 -0
- package/src/skills-manager.js +555 -0
- package/src/skills-scopes.js +809 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { COMMANDS_PATH } from './skills-scopes.js';
|
|
4
|
+
|
|
5
|
+
// Mock support for testing
|
|
6
|
+
const USE_MOCK_COMMANDS = process.env.MCP_USE_MOCK_SKILLS === 'true';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse YAML frontmatter from a command file
|
|
10
|
+
* Format:
|
|
11
|
+
* ---
|
|
12
|
+
* description: Command description
|
|
13
|
+
* tools: tool1, tool2
|
|
14
|
+
* ...other metadata
|
|
15
|
+
* ---
|
|
16
|
+
* Prompt content here
|
|
17
|
+
*/
|
|
18
|
+
function parseFrontmatter(content) {
|
|
19
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
20
|
+
|
|
21
|
+
if (!frontmatterMatch) {
|
|
22
|
+
return { metadata: {}, content: content.trim() };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [, yamlPart, bodyPart] = frontmatterMatch;
|
|
26
|
+
const metadata = {};
|
|
27
|
+
|
|
28
|
+
// Simple YAML parsing for key: value pairs
|
|
29
|
+
const lines = yamlPart.split('\n');
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
32
|
+
if (match) {
|
|
33
|
+
const [, key, value] = match;
|
|
34
|
+
// Handle arrays (comma-separated values)
|
|
35
|
+
if (value.includes(',')) {
|
|
36
|
+
metadata[key] = value.split(',').map(v => v.trim());
|
|
37
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
38
|
+
metadata[key] = value.slice(1, -1);
|
|
39
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
40
|
+
metadata[key] = value.slice(1, -1);
|
|
41
|
+
} else {
|
|
42
|
+
metadata[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { metadata, content: bodyPart.trim() };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate YAML frontmatter string from metadata object
|
|
52
|
+
*/
|
|
53
|
+
function generateFrontmatter(metadata) {
|
|
54
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const lines = ['---'];
|
|
59
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
lines.push(`${key}: ${value.join(', ')}`);
|
|
62
|
+
} else if (typeof value === 'string' && (value.includes(':') || value.includes('#') || value.includes('\n'))) {
|
|
63
|
+
lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
|
|
64
|
+
} else {
|
|
65
|
+
lines.push(`${key}: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
lines.push('---');
|
|
69
|
+
lines.push('');
|
|
70
|
+
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate command name to prevent path traversal attacks
|
|
76
|
+
*/
|
|
77
|
+
function validateCommandName(name) {
|
|
78
|
+
if (!name || name.includes('..') || name.includes('/') || name.includes('\\') || name.includes('\0')) {
|
|
79
|
+
throw new Error(`Invalid command name: '${name}'`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* CommandsManager handles CRUD operations for Claude Code slash commands
|
|
85
|
+
*/
|
|
86
|
+
export class CommandsManager {
|
|
87
|
+
constructor() {
|
|
88
|
+
this.mockCommandsInitialized = false;
|
|
89
|
+
this.commandsPath = COMMANDS_PATH;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async initializeMockCommands() {
|
|
93
|
+
if (USE_MOCK_COMMANDS && !this.mockCommandsInitialized) {
|
|
94
|
+
try {
|
|
95
|
+
const mockModule = await import('../test/mock-skills.js');
|
|
96
|
+
this.commandsPath = mockModule.MOCK_COMMANDS_PATH;
|
|
97
|
+
this.mockCommandsInitialized = true;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn('Mock commands not available, using production path');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async getCommandsPath() {
|
|
105
|
+
await this.initializeMockCommands();
|
|
106
|
+
return this.commandsPath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List all commands
|
|
111
|
+
*/
|
|
112
|
+
async listCommands() {
|
|
113
|
+
const commandsPath = await this.getCommandsPath();
|
|
114
|
+
const commands = [];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(commandsPath);
|
|
118
|
+
} catch {
|
|
119
|
+
return commands; // Commands directory doesn't exist
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entries = await fs.readdir(commandsPath, { withFileTypes: true });
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (entry.name.startsWith('.')) continue;
|
|
126
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const filePath = path.join(commandsPath, entry.name);
|
|
130
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
131
|
+
const { metadata } = parseFrontmatter(content);
|
|
132
|
+
|
|
133
|
+
// Command name is the filename without .md extension
|
|
134
|
+
const name = entry.name.replace(/\.md$/, '');
|
|
135
|
+
|
|
136
|
+
commands.push({
|
|
137
|
+
name,
|
|
138
|
+
path: filePath,
|
|
139
|
+
description: metadata.description || '',
|
|
140
|
+
tools: Array.isArray(metadata.tools) ? metadata.tools : (metadata.tools ? [metadata.tools] : []),
|
|
141
|
+
metadata
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.warn(`Error reading command ${entry.name}:`, error.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read a single command
|
|
153
|
+
*/
|
|
154
|
+
async readCommand(commandName) {
|
|
155
|
+
validateCommandName(commandName);
|
|
156
|
+
const commandsPath = await this.getCommandsPath();
|
|
157
|
+
const filePath = path.join(commandsPath, `${commandName}.md`);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
161
|
+
const { metadata, content: promptContent } = parseFrontmatter(content);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name: commandName,
|
|
165
|
+
path: filePath,
|
|
166
|
+
description: metadata.description || '',
|
|
167
|
+
tools: Array.isArray(metadata.tools) ? metadata.tools : (metadata.tools ? [metadata.tools] : []),
|
|
168
|
+
metadata,
|
|
169
|
+
content: promptContent,
|
|
170
|
+
fullContent: content
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error.code === 'ENOENT') {
|
|
174
|
+
throw new Error(`Command '${commandName}' not found`);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create or update a command
|
|
182
|
+
*/
|
|
183
|
+
async writeCommand(commandName, data) {
|
|
184
|
+
validateCommandName(commandName);
|
|
185
|
+
const commandsPath = await this.getCommandsPath();
|
|
186
|
+
|
|
187
|
+
// Ensure commands directory exists
|
|
188
|
+
await fs.mkdir(commandsPath, { recursive: true });
|
|
189
|
+
|
|
190
|
+
const { metadata = {}, content = '' } = data;
|
|
191
|
+
|
|
192
|
+
// Generate full content with frontmatter
|
|
193
|
+
const frontmatter = generateFrontmatter(metadata);
|
|
194
|
+
const fullContent = frontmatter + content;
|
|
195
|
+
|
|
196
|
+
const filePath = path.join(commandsPath, `${commandName}.md`);
|
|
197
|
+
await fs.writeFile(filePath, fullContent, 'utf-8');
|
|
198
|
+
|
|
199
|
+
return { path: filePath, written: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Delete a command
|
|
204
|
+
*/
|
|
205
|
+
async deleteCommand(commandName) {
|
|
206
|
+
validateCommandName(commandName);
|
|
207
|
+
const commandsPath = await this.getCommandsPath();
|
|
208
|
+
const filePath = path.join(commandsPath, `${commandName}.md`);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await fs.unlink(filePath);
|
|
212
|
+
return { deleted: true, path: filePath };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error.code === 'ENOENT') {
|
|
215
|
+
throw new Error(`Command '${commandName}' not found`);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Rename a command
|
|
223
|
+
*/
|
|
224
|
+
async renameCommand(oldName, newName) {
|
|
225
|
+
validateCommandName(oldName);
|
|
226
|
+
validateCommandName(newName);
|
|
227
|
+
const commandsPath = await this.getCommandsPath();
|
|
228
|
+
const oldPath = path.join(commandsPath, `${oldName}.md`);
|
|
229
|
+
const newPath = path.join(commandsPath, `${newName}.md`);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await fs.access(oldPath);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error(`Command '${oldName}' not found`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await fs.access(newPath);
|
|
239
|
+
throw new Error(`Command '${newName}' already exists`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error.code !== 'ENOENT' && !error.message.includes('already exists')) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (error.message.includes('already exists')) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await fs.rename(oldPath, newPath);
|
|
250
|
+
return { renamed: true, from: oldName, to: newName };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Duplicate a command
|
|
255
|
+
*/
|
|
256
|
+
async duplicateCommand(commandName, newName) {
|
|
257
|
+
validateCommandName(commandName);
|
|
258
|
+
validateCommandName(newName);
|
|
259
|
+
const command = await this.readCommand(commandName);
|
|
260
|
+
|
|
261
|
+
await this.writeCommand(newName, {
|
|
262
|
+
metadata: command.metadata,
|
|
263
|
+
content: command.content
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return { duplicated: true, from: commandName, to: newName };
|
|
267
|
+
}
|
|
268
|
+
}
|
package/src/config-manager.js
CHANGED
|
@@ -316,13 +316,11 @@ export class MCPConfigManager {
|
|
|
316
316
|
}
|
|
317
317
|
};
|
|
318
318
|
} else if (format === 'codex') {
|
|
319
|
-
// Preserve other Codex settings and
|
|
319
|
+
// Preserve other Codex settings and replace the mcp_servers table entirely
|
|
320
|
+
// (don't merge with original, otherwise deleted servers get reintroduced)
|
|
320
321
|
return {
|
|
321
322
|
...originalConfig,
|
|
322
|
-
mcp_servers:
|
|
323
|
-
...(originalConfig.mcp_servers || {}),
|
|
324
|
-
...normalizedConfig.servers
|
|
325
|
-
}
|
|
323
|
+
mcp_servers: normalizedConfig.servers
|
|
326
324
|
};
|
|
327
325
|
}
|
|
328
326
|
return originalConfig;
|
package/src/server.js
CHANGED
|
@@ -3,11 +3,15 @@ import cors from 'cors';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { MCPConfigManager } from './config-manager.js';
|
|
6
|
+
import { SkillsManager } from './skills-manager.js';
|
|
7
|
+
import { CommandsManager } from './commands-manager.js';
|
|
6
8
|
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = path.dirname(__filename);
|
|
9
11
|
|
|
10
12
|
const manager = new MCPConfigManager();
|
|
13
|
+
const skillsManager = new SkillsManager();
|
|
14
|
+
const commandsManager = new CommandsManager();
|
|
11
15
|
|
|
12
16
|
export function startServer(port = 3456) {
|
|
13
17
|
const app = express();
|
|
@@ -193,6 +197,302 @@ export function startServer(port = 3456) {
|
|
|
193
197
|
}
|
|
194
198
|
});
|
|
195
199
|
|
|
200
|
+
// Skills API Routes - Per-Client Architecture
|
|
201
|
+
|
|
202
|
+
// Get skill tabs for a specific client
|
|
203
|
+
app.get('/api/clients/:client/tabs', async (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
const tabs = await skillsManager.getClientTabs(req.params.client);
|
|
206
|
+
res.json({ tabs });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error.message.includes('not found')) {
|
|
209
|
+
return res.status(404).json({ error: error.message });
|
|
210
|
+
}
|
|
211
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
212
|
+
return res.status(400).json({ error: error.message });
|
|
213
|
+
}
|
|
214
|
+
res.status(500).json({ error: error.message });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Check if client has skills support
|
|
219
|
+
app.get('/api/clients/:client/has-skills', async (req, res) => {
|
|
220
|
+
try {
|
|
221
|
+
const hasSkills = await skillsManager.clientHasSkills(req.params.client);
|
|
222
|
+
res.json({ hasSkills });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error.message.includes('not found')) {
|
|
225
|
+
return res.status(404).json({ error: error.message });
|
|
226
|
+
}
|
|
227
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
228
|
+
return res.status(400).json({ error: error.message });
|
|
229
|
+
}
|
|
230
|
+
res.status(500).json({ error: error.message });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// List items in a client's tab
|
|
235
|
+
app.get('/api/clients/:client/tabs/:tab/items', async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const items = await skillsManager.listItems(req.params.client, req.params.tab);
|
|
238
|
+
res.json({ items });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.message.includes('not found')) {
|
|
241
|
+
return res.status(404).json({ error: error.message });
|
|
242
|
+
}
|
|
243
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
244
|
+
return res.status(400).json({ error: error.message });
|
|
245
|
+
}
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Get single item
|
|
251
|
+
app.get('/api/clients/:client/tabs/:tab/items/:name', async (req, res) => {
|
|
252
|
+
try {
|
|
253
|
+
const item = await skillsManager.readItem(req.params.client, req.params.tab, req.params.name);
|
|
254
|
+
res.json(item);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error.message.includes('not found')) {
|
|
257
|
+
return res.status(404).json({ error: error.message });
|
|
258
|
+
}
|
|
259
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
260
|
+
return res.status(400).json({ error: error.message });
|
|
261
|
+
}
|
|
262
|
+
res.status(500).json({ error: error.message });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Create item
|
|
267
|
+
app.post('/api/clients/:client/tabs/:tab/items/:name', async (req, res) => {
|
|
268
|
+
try {
|
|
269
|
+
const result = await skillsManager.writeItem(req.params.client, req.params.tab, req.params.name, req.body);
|
|
270
|
+
res.json({ success: true, ...result });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error.message.includes('not found')) {
|
|
273
|
+
return res.status(404).json({ error: error.message });
|
|
274
|
+
}
|
|
275
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
276
|
+
return res.status(400).json({ error: error.message });
|
|
277
|
+
}
|
|
278
|
+
res.status(500).json({ error: error.message });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Update item
|
|
283
|
+
app.put('/api/clients/:client/tabs/:tab/items/:name', async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const result = await skillsManager.writeItem(req.params.client, req.params.tab, req.params.name, req.body);
|
|
286
|
+
res.json({ success: true, ...result });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error.message.includes('not found')) {
|
|
289
|
+
return res.status(404).json({ error: error.message });
|
|
290
|
+
}
|
|
291
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
292
|
+
return res.status(400).json({ error: error.message });
|
|
293
|
+
}
|
|
294
|
+
res.status(500).json({ error: error.message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Delete item
|
|
299
|
+
app.delete('/api/clients/:client/tabs/:tab/items/:name', async (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
const result = await skillsManager.deleteItem(req.params.client, req.params.tab, req.params.name);
|
|
302
|
+
res.json({ success: true, ...result });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.message.includes('not found')) {
|
|
305
|
+
return res.status(404).json({ error: error.message });
|
|
306
|
+
}
|
|
307
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
308
|
+
return res.status(400).json({ error: error.message });
|
|
309
|
+
}
|
|
310
|
+
res.status(500).json({ error: error.message });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// List files in a complex item
|
|
315
|
+
app.get('/api/clients/:client/tabs/:tab/items/:name/files', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const files = await skillsManager.listItemFiles(req.params.client, req.params.tab, req.params.name);
|
|
318
|
+
res.json({ files });
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error.message.includes('not found')) {
|
|
321
|
+
return res.status(404).json({ error: error.message });
|
|
322
|
+
}
|
|
323
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
324
|
+
return res.status(400).json({ error: error.message });
|
|
325
|
+
}
|
|
326
|
+
res.status(500).json({ error: error.message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Read file from complex item - Express 5 syntax: /*paramName for wildcard
|
|
331
|
+
app.get('/api/clients/:client/tabs/:tab/items/:name/files/*filePath', async (req, res) => {
|
|
332
|
+
try {
|
|
333
|
+
// filePath is an array of path segments in Express 5, join them
|
|
334
|
+
const filePath = Array.isArray(req.params.filePath)
|
|
335
|
+
? req.params.filePath.join('/')
|
|
336
|
+
: req.params.filePath;
|
|
337
|
+
const file = await skillsManager.readItemFile(req.params.client, req.params.tab, req.params.name, filePath);
|
|
338
|
+
res.json(file);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (error.message.includes('not found')) {
|
|
341
|
+
return res.status(404).json({ error: error.message });
|
|
342
|
+
}
|
|
343
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
344
|
+
return res.status(400).json({ error: error.message });
|
|
345
|
+
}
|
|
346
|
+
res.status(500).json({ error: error.message });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Write file in complex item - Express 5 syntax: /*paramName for wildcard
|
|
351
|
+
app.put('/api/clients/:client/tabs/:tab/items/:name/files/*filePath', async (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
// filePath will be an array of path segments, join them
|
|
354
|
+
const filePath = Array.isArray(req.params.filePath)
|
|
355
|
+
? req.params.filePath.join('/')
|
|
356
|
+
: req.params.filePath;
|
|
357
|
+
const { content } = req.body;
|
|
358
|
+
const result = await skillsManager.writeItemFile(req.params.client, req.params.tab, req.params.name, filePath, content);
|
|
359
|
+
res.json({ success: true, ...result });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error.message.includes('not found')) {
|
|
362
|
+
return res.status(404).json({ error: error.message });
|
|
363
|
+
}
|
|
364
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
365
|
+
return res.status(400).json({ error: error.message });
|
|
366
|
+
}
|
|
367
|
+
res.status(500).json({ error: error.message });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Copy item within same tab
|
|
372
|
+
app.post('/api/clients/:client/tabs/:tab/items/:name/copy', async (req, res) => {
|
|
373
|
+
try {
|
|
374
|
+
const { newName } = req.body;
|
|
375
|
+
const result = await skillsManager.copyItem(req.params.client, req.params.tab, req.params.name, newName);
|
|
376
|
+
res.json({ success: true, ...result });
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.message.includes('not found')) {
|
|
379
|
+
return res.status(404).json({ error: error.message });
|
|
380
|
+
}
|
|
381
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
382
|
+
return res.status(400).json({ error: error.message });
|
|
383
|
+
}
|
|
384
|
+
res.status(500).json({ error: error.message });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Commands API Routes
|
|
389
|
+
app.get('/api/commands', async (req, res) => {
|
|
390
|
+
try {
|
|
391
|
+
const commands = await commandsManager.listCommands();
|
|
392
|
+
res.json({ commands });
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (error.message.includes('not found')) {
|
|
395
|
+
return res.status(404).json({ error: error.message });
|
|
396
|
+
}
|
|
397
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
398
|
+
return res.status(400).json({ error: error.message });
|
|
399
|
+
}
|
|
400
|
+
res.status(500).json({ error: error.message });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
app.get('/api/commands/:name', async (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
const command = await commandsManager.readCommand(req.params.name);
|
|
407
|
+
res.json(command);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error.message.includes('not found')) {
|
|
410
|
+
return res.status(404).json({ error: error.message });
|
|
411
|
+
}
|
|
412
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
413
|
+
return res.status(400).json({ error: error.message });
|
|
414
|
+
}
|
|
415
|
+
res.status(500).json({ error: error.message });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
app.post('/api/commands/:name', async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
await commandsManager.writeCommand(req.params.name, req.body);
|
|
422
|
+
res.json({ success: true });
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error.message.includes('not found')) {
|
|
425
|
+
return res.status(404).json({ error: error.message });
|
|
426
|
+
}
|
|
427
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
428
|
+
return res.status(400).json({ error: error.message });
|
|
429
|
+
}
|
|
430
|
+
res.status(500).json({ error: error.message });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
app.put('/api/commands/:name', async (req, res) => {
|
|
435
|
+
try {
|
|
436
|
+
await commandsManager.writeCommand(req.params.name, req.body);
|
|
437
|
+
res.json({ success: true });
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (error.message.includes('not found')) {
|
|
440
|
+
return res.status(404).json({ error: error.message });
|
|
441
|
+
}
|
|
442
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
443
|
+
return res.status(400).json({ error: error.message });
|
|
444
|
+
}
|
|
445
|
+
res.status(500).json({ error: error.message });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
app.delete('/api/commands/:name', async (req, res) => {
|
|
450
|
+
try {
|
|
451
|
+
await commandsManager.deleteCommand(req.params.name);
|
|
452
|
+
res.json({ success: true });
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error.message.includes('not found')) {
|
|
455
|
+
return res.status(404).json({ error: error.message });
|
|
456
|
+
}
|
|
457
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
458
|
+
return res.status(400).json({ error: error.message });
|
|
459
|
+
}
|
|
460
|
+
res.status(500).json({ error: error.message });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
app.post('/api/commands/:name/rename', async (req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
const { newName } = req.body;
|
|
467
|
+
await commandsManager.renameCommand(req.params.name, newName);
|
|
468
|
+
res.json({ success: true });
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (error.message.includes('not found')) {
|
|
471
|
+
return res.status(404).json({ error: error.message });
|
|
472
|
+
}
|
|
473
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
474
|
+
return res.status(400).json({ error: error.message });
|
|
475
|
+
}
|
|
476
|
+
res.status(500).json({ error: error.message });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
app.post('/api/commands/:name/duplicate', async (req, res) => {
|
|
481
|
+
try {
|
|
482
|
+
const { newName } = req.body;
|
|
483
|
+
await commandsManager.duplicateCommand(req.params.name, newName);
|
|
484
|
+
res.json({ success: true });
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error.message.includes('not found')) {
|
|
487
|
+
return res.status(404).json({ error: error.message });
|
|
488
|
+
}
|
|
489
|
+
if (error.message.includes('Invalid') || error.message.includes('already exists')) {
|
|
490
|
+
return res.status(400).json({ error: error.message });
|
|
491
|
+
}
|
|
492
|
+
res.status(500).json({ error: error.message });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
196
496
|
app.listen(port, () => {
|
|
197
497
|
console.log(`MCP Config Manager server running on http://localhost:${port}`);
|
|
198
498
|
});
|