mcpsmgr 0.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1631 -0
- package/dist/index.js.map +1 -0
- package/docs/README_zh-CN.md +99 -0
- package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/design.md +41 -0
- package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/proposal.md +28 -0
- package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/specs/project-operations/spec.md +53 -0
- package/openspec/changes/archive/2026-03-12-fix-global-mcp-default-selection/tasks.md +9 -0
- package/openspec/changes/archive/2026-03-12-fix-init-server-detection/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-03-12-fix-init-server-detection/design.md +40 -0
- package/openspec/changes/archive/2026-03-12-fix-init-server-detection/proposal.md +25 -0
- package/openspec/changes/archive/2026-03-12-fix-init-server-detection/specs/project-operations/spec.md +25 -0
- package/openspec/changes/archive/2026-03-12-fix-init-server-detection/tasks.md +10 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/design.md +32 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/proposal.md +25 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/specs/project-operations/spec.md +30 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/specs/server-management/spec.md +15 -0
- package/openspec/changes/archive/2026-03-12-graceful-exit-on-interrupt/tasks.md +17 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/design.md +104 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/proposal.md +34 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/agent-adapters/spec.md +110 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/central-storage/spec.md +38 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/glm-integration/spec.md +66 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/project-operations/spec.md +76 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/specs/server-management/spec.md +75 -0
- package/openspec/changes/archive/2026-03-12-mcps-manager-cli/tasks.md +60 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/agent-adapters/spec.md +148 -0
- package/openspec/specs/central-storage/spec.md +42 -0
- package/openspec/specs/glm-integration/spec.md +70 -0
- package/openspec/specs/project-operations/spec.md +138 -0
- package/openspec/specs/server-management/spec.md +93 -0
- package/package.json +33 -0
- package/src/__tests__/integration.test.ts +200 -0
- package/src/adapters/__tests__/adapters.test.ts +274 -0
- package/src/adapters/antigravity.ts +114 -0
- package/src/adapters/claude-code.ts +114 -0
- package/src/adapters/codex-cli.ts +135 -0
- package/src/adapters/env-args.ts +51 -0
- package/src/adapters/gemini-cli.ts +110 -0
- package/src/adapters/index.ts +32 -0
- package/src/adapters/json-file.ts +24 -0
- package/src/adapters/opencode.ts +114 -0
- package/src/commands/add.ts +68 -0
- package/src/commands/init.ts +136 -0
- package/src/commands/list.ts +77 -0
- package/src/commands/remove.ts +61 -0
- package/src/commands/server-add.ts +211 -0
- package/src/commands/server-list.ts +24 -0
- package/src/commands/server-remove.ts +12 -0
- package/src/commands/setup.ts +71 -0
- package/src/commands/sync.ts +98 -0
- package/src/index.ts +100 -0
- package/src/services/glm-client.ts +190 -0
- package/src/services/system-prompt.ts +61 -0
- package/src/services/web-reader.ts +130 -0
- package/src/types.ts +59 -0
- package/src/utils/config.ts +22 -0
- package/src/utils/paths.ts +11 -0
- package/src/utils/prompt.ts +3 -0
- package/src/utils/resolve-config.ts +13 -0
- package/src/utils/server-store.ts +56 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +13 -0
- package/vitest.config.ts +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1631 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/utils/config.ts
|
|
7
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
|
|
10
|
+
// src/utils/paths.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var BASE_DIR = join(homedir(), ".mcps-manager");
|
|
14
|
+
var paths = {
|
|
15
|
+
baseDir: BASE_DIR,
|
|
16
|
+
serversDir: join(BASE_DIR, "servers"),
|
|
17
|
+
configFile: join(BASE_DIR, "config.json"),
|
|
18
|
+
serverFile: (name) => join(BASE_DIR, "servers", `${name}.json`)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/utils/config.ts
|
|
22
|
+
async function readGlobalConfig() {
|
|
23
|
+
const raw = await readFile(paths.configFile, "utf-8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
async function writeGlobalConfig(config) {
|
|
27
|
+
if (!existsSync(paths.baseDir)) {
|
|
28
|
+
await mkdir(paths.baseDir, { recursive: true });
|
|
29
|
+
await chmod(paths.baseDir, 448);
|
|
30
|
+
}
|
|
31
|
+
await writeFile(paths.configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
32
|
+
await chmod(paths.configFile, 384);
|
|
33
|
+
}
|
|
34
|
+
function configExists() {
|
|
35
|
+
return existsSync(paths.configFile);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/commands/setup.ts
|
|
39
|
+
import { mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
|
|
40
|
+
import { existsSync as existsSync2 } from "fs";
|
|
41
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
42
|
+
|
|
43
|
+
// src/utils/prompt.ts
|
|
44
|
+
function isUserCancellation(error) {
|
|
45
|
+
return error instanceof Error && error.name === "ExitPromptError";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/commands/setup.ts
|
|
49
|
+
var GLM_ENDPOINTS = [
|
|
50
|
+
{
|
|
51
|
+
name: "Coding Plan (GLM-5)",
|
|
52
|
+
value: "https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "General (GLM-5)",
|
|
56
|
+
value: "https://open.bigmodel.cn/api/paas/v4/chat/completions"
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
async function setupCommand() {
|
|
60
|
+
try {
|
|
61
|
+
await setupCommandInner();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isUserCancellation(error)) return;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function setupCommandInner() {
|
|
68
|
+
if (configExists()) {
|
|
69
|
+
const overwrite = await confirm({
|
|
70
|
+
message: "Configuration already exists. Overwrite?"
|
|
71
|
+
});
|
|
72
|
+
if (!overwrite) {
|
|
73
|
+
console.log("Setup cancelled.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync2(paths.baseDir)) {
|
|
78
|
+
await mkdir2(paths.baseDir, { recursive: true });
|
|
79
|
+
await chmod2(paths.baseDir, 448);
|
|
80
|
+
}
|
|
81
|
+
if (!existsSync2(paths.serversDir)) {
|
|
82
|
+
await mkdir2(paths.serversDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const glmApiKey = await input({
|
|
85
|
+
message: "Enter GLM5 API Key:",
|
|
86
|
+
validate: (v) => v.trim().length > 0 ? true : "API key is required"
|
|
87
|
+
});
|
|
88
|
+
const glmEndpoint = await select({
|
|
89
|
+
message: "Select GLM5 endpoint:",
|
|
90
|
+
choices: GLM_ENDPOINTS.map((e) => ({ name: e.name, value: e.value }))
|
|
91
|
+
});
|
|
92
|
+
const config = {
|
|
93
|
+
glm: {
|
|
94
|
+
apiKey: glmApiKey.trim(),
|
|
95
|
+
endpoint: glmEndpoint
|
|
96
|
+
},
|
|
97
|
+
webReader: {
|
|
98
|
+
apiKey: glmApiKey.trim(),
|
|
99
|
+
url: "https://open.bigmodel.cn/api/mcp/web_reader/mcp"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await writeGlobalConfig(config);
|
|
103
|
+
console.log("Setup complete. Configuration saved to ~/.mcps-manager/config.json");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/commands/server-add.ts
|
|
107
|
+
import { confirm as confirm2, input as input2, password } from "@inquirer/prompts";
|
|
108
|
+
|
|
109
|
+
// src/utils/server-store.ts
|
|
110
|
+
import { readFile as readFile2, writeFile as writeFile2, readdir, unlink, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
|
|
111
|
+
import { existsSync as existsSync3 } from "fs";
|
|
112
|
+
async function readServerDefinition(name) {
|
|
113
|
+
const filePath = paths.serverFile(name);
|
|
114
|
+
if (!existsSync3(filePath)) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
118
|
+
return JSON.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
async function writeServerDefinition(definition) {
|
|
121
|
+
if (!existsSync3(paths.serversDir)) {
|
|
122
|
+
await mkdir3(paths.serversDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
const filePath = paths.serverFile(definition.name);
|
|
125
|
+
await writeFile2(filePath, JSON.stringify(definition, null, 2), "utf-8");
|
|
126
|
+
await chmod3(filePath, 384);
|
|
127
|
+
}
|
|
128
|
+
async function removeServerDefinition(name) {
|
|
129
|
+
const filePath = paths.serverFile(name);
|
|
130
|
+
if (!existsSync3(filePath)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await unlink(filePath);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
async function listServerDefinitions() {
|
|
137
|
+
if (!existsSync3(paths.serversDir)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const files = await readdir(paths.serversDir);
|
|
141
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
142
|
+
const results = [];
|
|
143
|
+
for (const file of jsonFiles) {
|
|
144
|
+
const raw = await readFile2(
|
|
145
|
+
paths.serverFile(file.replace(".json", "")),
|
|
146
|
+
"utf-8"
|
|
147
|
+
);
|
|
148
|
+
results.push(JSON.parse(raw));
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
function serverExists(name) {
|
|
153
|
+
return existsSync3(paths.serverFile(name));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/services/web-reader.ts
|
|
157
|
+
function parseSseResponse(raw) {
|
|
158
|
+
const lines = raw.split("\n");
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
if (line.startsWith("data:")) {
|
|
161
|
+
return JSON.parse(line.slice(5));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
async function mcpInitialize(endpoint, apiKey) {
|
|
167
|
+
const response = await fetch(endpoint, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
Accept: "application/json, text/event-stream",
|
|
172
|
+
Authorization: `Bearer ${apiKey}`
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
jsonrpc: "2.0",
|
|
176
|
+
id: 1,
|
|
177
|
+
method: "initialize",
|
|
178
|
+
params: {
|
|
179
|
+
protocolVersion: "2025-03-26",
|
|
180
|
+
capabilities: {},
|
|
181
|
+
clientInfo: { name: "mcpsmgr", version: "0.1.0" }
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`MCP initialize failed: ${response.status} ${response.statusText}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
throw new Error("MCP server did not return session ID");
|
|
193
|
+
}
|
|
194
|
+
return sessionId;
|
|
195
|
+
}
|
|
196
|
+
async function mcpToolCall(endpoint, apiKey, sessionId, toolName, args) {
|
|
197
|
+
const response = await fetch(endpoint, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
Accept: "application/json, text/event-stream",
|
|
202
|
+
Authorization: `Bearer ${apiKey}`,
|
|
203
|
+
"Mcp-Session-Id": sessionId
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id: 2,
|
|
208
|
+
method: "tools/call",
|
|
209
|
+
params: { name: toolName, arguments: args }
|
|
210
|
+
})
|
|
211
|
+
});
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`MCP tools/call failed: ${response.status} ${response.statusText}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const raw = await response.text();
|
|
218
|
+
const parsed = parseSseResponse(raw);
|
|
219
|
+
if (parsed.error) {
|
|
220
|
+
throw new Error(`MCP error: ${parsed.error.message}`);
|
|
221
|
+
}
|
|
222
|
+
const contents = parsed.result?.content ?? [];
|
|
223
|
+
const full = contents.map((c) => c.text).join("\n");
|
|
224
|
+
const MAX_CONTENT_LENGTH = 3e4;
|
|
225
|
+
if (full.length > MAX_CONTENT_LENGTH) {
|
|
226
|
+
return full.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Content truncated]";
|
|
227
|
+
}
|
|
228
|
+
return full;
|
|
229
|
+
}
|
|
230
|
+
var cachedSessionId;
|
|
231
|
+
async function fetchWebContent(config, url) {
|
|
232
|
+
const endpoint = config.webReader.url;
|
|
233
|
+
const apiKey = config.webReader.apiKey;
|
|
234
|
+
if (!cachedSessionId) {
|
|
235
|
+
cachedSessionId = await mcpInitialize(endpoint, apiKey);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
return await mcpToolCall(endpoint, apiKey, cachedSessionId, "webReader", {
|
|
239
|
+
url,
|
|
240
|
+
timeout: 20
|
|
241
|
+
});
|
|
242
|
+
} catch {
|
|
243
|
+
cachedSessionId = await mcpInitialize(endpoint, apiKey);
|
|
244
|
+
return await mcpToolCall(endpoint, apiKey, cachedSessionId, "webReader", {
|
|
245
|
+
url,
|
|
246
|
+
timeout: 20
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/services/system-prompt.ts
|
|
252
|
+
var ANALYSIS_SYSTEM_PROMPT = `You are an MCP (Model Context Protocol) server configuration analyst. Your task is to analyze documentation for an MCP server and extract configuration details for 5 different coding agents.
|
|
253
|
+
|
|
254
|
+
The 5 agents and their configuration differences:
|
|
255
|
+
|
|
256
|
+
1. **Claude Code** (.mcp.json)
|
|
257
|
+
- Format: { "type": "stdio"|"http", "command": "...", "args": [...] }
|
|
258
|
+
- HTTP: { "type": "http", "url": "...", "headers": {...} }
|
|
259
|
+
- IMPORTANT: Do NOT use "env" field. Environment variables will be handled separately.
|
|
260
|
+
|
|
261
|
+
2. **Codex CLI** (.codex/config.toml)
|
|
262
|
+
- TOML format: command = "...", args = [...]
|
|
263
|
+
- Same key names as Claude Code but in TOML
|
|
264
|
+
- IMPORTANT: Do NOT use "env" field.
|
|
265
|
+
|
|
266
|
+
3. **Gemini CLI** (.gemini/settings.json)
|
|
267
|
+
- Format: { "command": "...", "args": [...] }
|
|
268
|
+
- No "type" field needed
|
|
269
|
+
- IMPORTANT: Do NOT use "env" field.
|
|
270
|
+
|
|
271
|
+
4. **OpenCode** (opencode.json)
|
|
272
|
+
- Format: { "type": "local"|"remote", "command": ["cmd", "arg1", ...] }
|
|
273
|
+
- command is an ARRAY including the command itself
|
|
274
|
+
- type is "local" for stdio, "remote" for http
|
|
275
|
+
- IMPORTANT: Do NOT use "environment" field.
|
|
276
|
+
|
|
277
|
+
5. **Antigravity** (~/.gemini/antigravity/mcp_config.json)
|
|
278
|
+
- Format: { "command": "...", "args": [...] }
|
|
279
|
+
- HTTP: { "serverUrl": "...", "headers": {...} } (note: "serverUrl" not "url")
|
|
280
|
+
- IMPORTANT: Do NOT use "env" field.
|
|
281
|
+
|
|
282
|
+
You have access to a webReader tool to fetch web page content. Use it to read the documentation URL provided.
|
|
283
|
+
|
|
284
|
+
After analyzing the documentation, return a JSON object with this exact structure:
|
|
285
|
+
\`\`\`json
|
|
286
|
+
{
|
|
287
|
+
"name": "server-name",
|
|
288
|
+
"default": {
|
|
289
|
+
"transport": "stdio",
|
|
290
|
+
"command": "npx",
|
|
291
|
+
"args": ["-y", "@scope/package"],
|
|
292
|
+
},
|
|
293
|
+
"overrides": {
|
|
294
|
+
"opencode": {
|
|
295
|
+
"transport": "stdio",
|
|
296
|
+
"command": "npx",
|
|
297
|
+
"args": ["-y", "@scope/package"],
|
|
298
|
+
"env": {}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
"requiredEnvVars": ["API_KEY"]
|
|
302
|
+
}
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
Rules:
|
|
306
|
+
- "name" should be a kebab-case identifier for the server
|
|
307
|
+
- "default" should be the most common configuration (usually works for Claude Code, Codex CLI, Gemini CLI)
|
|
308
|
+
- Only add "overrides" for agents that need DIFFERENT configuration from the default
|
|
309
|
+
- OpenCode usually needs an override because its command format differs (array vs string+args)
|
|
310
|
+
- "requiredEnvVars" lists environment variable names the user needs to provide values for
|
|
311
|
+
- Transport is either "stdio" or "http"
|
|
312
|
+
- Return ONLY the JSON object, no markdown fences, no explanation`;
|
|
313
|
+
|
|
314
|
+
// src/services/glm-client.ts
|
|
315
|
+
var WEB_READER_TOOL = {
|
|
316
|
+
type: "function",
|
|
317
|
+
function: {
|
|
318
|
+
name: "webReader",
|
|
319
|
+
description: "Fetch and read the content of a web page given its URL",
|
|
320
|
+
parameters: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
url: {
|
|
324
|
+
type: "string",
|
|
325
|
+
description: "The URL of the web page to read"
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
required: ["url"]
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
async function callGlm(config, messages) {
|
|
333
|
+
const response = await fetch(config.glm.endpoint, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
Authorization: `Bearer ${config.glm.apiKey}`
|
|
338
|
+
},
|
|
339
|
+
body: JSON.stringify({
|
|
340
|
+
model: "glm-5",
|
|
341
|
+
messages,
|
|
342
|
+
tools: [WEB_READER_TOOL],
|
|
343
|
+
tool_choice: "auto"
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`GLM5 API request failed: ${response.status} ${response.statusText}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return await response.json();
|
|
352
|
+
}
|
|
353
|
+
async function analyzeWithGlm(config, userMessage) {
|
|
354
|
+
const messages = [
|
|
355
|
+
{ role: "system", content: ANALYSIS_SYSTEM_PROMPT },
|
|
356
|
+
{ role: "user", content: userMessage }
|
|
357
|
+
];
|
|
358
|
+
const MAX_ROUNDS = 10;
|
|
359
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
360
|
+
const response = await callGlm(config, messages);
|
|
361
|
+
const choice = response.choices.at(0);
|
|
362
|
+
if (!choice) {
|
|
363
|
+
throw new Error("GLM5 returned empty response");
|
|
364
|
+
}
|
|
365
|
+
const assistantMessage = choice.message;
|
|
366
|
+
messages.push({
|
|
367
|
+
role: "assistant",
|
|
368
|
+
content: assistantMessage.content,
|
|
369
|
+
tool_calls: assistantMessage.tool_calls
|
|
370
|
+
});
|
|
371
|
+
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
372
|
+
return parseAnalysisResult(assistantMessage.content ?? "");
|
|
373
|
+
}
|
|
374
|
+
for (const toolCall of assistantMessage.tool_calls) {
|
|
375
|
+
if (toolCall.function.name === "webReader") {
|
|
376
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
377
|
+
let toolResult;
|
|
378
|
+
try {
|
|
379
|
+
toolResult = await fetchWebContent(config, args.url);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
toolResult = `Error fetching URL: ${error instanceof Error ? error.message : String(error)}`;
|
|
382
|
+
}
|
|
383
|
+
messages.push({
|
|
384
|
+
role: "tool",
|
|
385
|
+
content: toolResult,
|
|
386
|
+
tool_call_id: toolCall.id
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
throw new Error("GLM5 analysis exceeded maximum rounds");
|
|
392
|
+
}
|
|
393
|
+
function parseAnalysisResult(content) {
|
|
394
|
+
const cleaned = content.replace(/```json\s*/g, "").replace(/```\s*/g, "").trim();
|
|
395
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
396
|
+
if (!jsonMatch) {
|
|
397
|
+
throw new Error(`Cannot extract JSON from GLM5 response: ${cleaned.slice(0, 200)}`);
|
|
398
|
+
}
|
|
399
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
400
|
+
if (!result.name || !result.default) {
|
|
401
|
+
throw new Error("Invalid analysis result: missing name or default config");
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
function buildUserMessage(input3) {
|
|
406
|
+
if (isGitHubRepo(input3)) {
|
|
407
|
+
const url = `https://github.com/${input3}`;
|
|
408
|
+
return `Please analyze the MCP server at ${url}. Start by reading the README at ${url}/blob/main/README.md`;
|
|
409
|
+
}
|
|
410
|
+
return `Please analyze the MCP server documentation at: ${input3}`;
|
|
411
|
+
}
|
|
412
|
+
function isGitHubRepo(input3) {
|
|
413
|
+
if (input3.startsWith("http") || input3.startsWith("@")) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
const parts = input3.split("/");
|
|
417
|
+
return parts.length === 2 && parts.every((p) => p.length > 0);
|
|
418
|
+
}
|
|
419
|
+
function isValidInput(input3) {
|
|
420
|
+
if (input3.startsWith("http://") || input3.startsWith("https://")) {
|
|
421
|
+
return { valid: true };
|
|
422
|
+
}
|
|
423
|
+
if (isGitHubRepo(input3)) {
|
|
424
|
+
return { valid: true };
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
valid: false,
|
|
428
|
+
reason: 'Invalid input format. Provide a URL (https://...) or GitHub owner/repo (e.g., "anthropics/mcp-brave-search")'
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/commands/server-add.ts
|
|
433
|
+
async function serverAddCommand(source) {
|
|
434
|
+
try {
|
|
435
|
+
await serverAddCommandInner(source);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (isUserCancellation(error)) return;
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function serverAddCommandInner(source) {
|
|
442
|
+
const urlInput = source ?? await input2({
|
|
443
|
+
message: "Enter MCP server URL or GitHub owner/repo (leave empty for manual):"
|
|
444
|
+
});
|
|
445
|
+
if (urlInput.trim() === "") {
|
|
446
|
+
await manualAddFlow();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const validation = isValidInput(urlInput.trim());
|
|
450
|
+
if (!validation.valid) {
|
|
451
|
+
console.error(`Error: ${validation.reason}`);
|
|
452
|
+
process.exitCode = 1;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const config = await readGlobalConfig();
|
|
456
|
+
console.log("Analyzing documentation with GLM5...");
|
|
457
|
+
let analysis;
|
|
458
|
+
try {
|
|
459
|
+
const userMessage = buildUserMessage(urlInput.trim());
|
|
460
|
+
analysis = await analyzeWithGlm(config, userMessage);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(
|
|
463
|
+
`GLM5 analysis failed: ${error instanceof Error ? error.message : String(error)}`
|
|
464
|
+
);
|
|
465
|
+
const fallback = await confirm2({
|
|
466
|
+
message: "Would you like to configure manually instead?"
|
|
467
|
+
});
|
|
468
|
+
if (fallback) {
|
|
469
|
+
await manualAddFlow();
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
displayAnalysisResult(analysis, urlInput.trim());
|
|
474
|
+
const trust = await confirm2({
|
|
475
|
+
message: "Trust this analysis result?"
|
|
476
|
+
});
|
|
477
|
+
if (!trust) {
|
|
478
|
+
const manual = await confirm2({
|
|
479
|
+
message: "Configure manually instead?"
|
|
480
|
+
});
|
|
481
|
+
if (manual) {
|
|
482
|
+
await manualAddFlow();
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (serverExists(analysis.name)) {
|
|
487
|
+
console.error(
|
|
488
|
+
`Error: Server "${analysis.name}" already exists. Run "mcpsmgr server remove ${analysis.name}" first.`
|
|
489
|
+
);
|
|
490
|
+
process.exitCode = 1;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const env = {};
|
|
494
|
+
for (const varName of analysis.requiredEnvVars) {
|
|
495
|
+
const value = await password({
|
|
496
|
+
message: `Enter value for ${varName} (stored locally, never sent to servers):`,
|
|
497
|
+
mask: "*"
|
|
498
|
+
});
|
|
499
|
+
env[varName] = value;
|
|
500
|
+
}
|
|
501
|
+
const defaultConfig = analysis.default.transport === "stdio" ? {
|
|
502
|
+
transport: "stdio",
|
|
503
|
+
command: analysis.default.command ?? "",
|
|
504
|
+
args: [...analysis.default.args ?? []],
|
|
505
|
+
env: { ...analysis.default.env ?? {}, ...env }
|
|
506
|
+
} : {
|
|
507
|
+
transport: "http",
|
|
508
|
+
url: analysis.default.url ?? "",
|
|
509
|
+
headers: { ...analysis.default.headers ?? {} }
|
|
510
|
+
};
|
|
511
|
+
const definition = {
|
|
512
|
+
name: analysis.name,
|
|
513
|
+
source: urlInput.trim(),
|
|
514
|
+
default: defaultConfig,
|
|
515
|
+
overrides: analysis.overrides
|
|
516
|
+
};
|
|
517
|
+
await writeServerDefinition(definition);
|
|
518
|
+
console.log(`Server "${analysis.name}" saved to central repository.`);
|
|
519
|
+
}
|
|
520
|
+
function displayAnalysisResult(result, source) {
|
|
521
|
+
console.log("\n--- Analysis Result ---");
|
|
522
|
+
console.log(`Name: ${result.name}`);
|
|
523
|
+
console.log(`Source: ${source}`);
|
|
524
|
+
console.log(`Transport: ${result.default.transport}`);
|
|
525
|
+
if (result.default.transport === "stdio") {
|
|
526
|
+
console.log(`Command: ${result.default.command}`);
|
|
527
|
+
console.log(`Args: ${JSON.stringify(result.default.args)}`);
|
|
528
|
+
} else {
|
|
529
|
+
console.log(`URL: ${result.default.url}`);
|
|
530
|
+
}
|
|
531
|
+
if (Object.keys(result.overrides).length > 0) {
|
|
532
|
+
console.log("Agent overrides:");
|
|
533
|
+
for (const [agent, override] of Object.entries(result.overrides)) {
|
|
534
|
+
console.log(` ${agent}: ${JSON.stringify(override)}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (result.requiredEnvVars.length > 0) {
|
|
538
|
+
console.log(
|
|
539
|
+
`Required env vars: ${result.requiredEnvVars.join(", ")}`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
console.log("---\n");
|
|
543
|
+
}
|
|
544
|
+
async function manualAddFlow() {
|
|
545
|
+
const name = await input2({
|
|
546
|
+
message: "Server name (kebab-case):",
|
|
547
|
+
validate: (v) => /^[a-z][a-z0-9-]*$/.test(v.trim()) ? true : "Must be kebab-case"
|
|
548
|
+
});
|
|
549
|
+
if (serverExists(name.trim())) {
|
|
550
|
+
console.error(
|
|
551
|
+
`Error: Server "${name.trim()}" already exists. Run "mcpsmgr server remove ${name.trim()}" first.`
|
|
552
|
+
);
|
|
553
|
+
process.exitCode = 1;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const source = await input2({
|
|
557
|
+
message: "Source URL (optional):"
|
|
558
|
+
});
|
|
559
|
+
const command = await input2({
|
|
560
|
+
message: "Command (e.g., npx):",
|
|
561
|
+
validate: (v) => v.trim().length > 0 ? true : "Command is required"
|
|
562
|
+
});
|
|
563
|
+
const argsStr = await input2({
|
|
564
|
+
message: "Args (comma-separated, e.g., -y,@scope/package):"
|
|
565
|
+
});
|
|
566
|
+
const args = argsStr.trim().split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
567
|
+
const envPairs = {};
|
|
568
|
+
let addMore = true;
|
|
569
|
+
while (addMore) {
|
|
570
|
+
const envName = await input2({
|
|
571
|
+
message: "Env var name (leave empty to finish):"
|
|
572
|
+
});
|
|
573
|
+
if (envName.trim() === "") break;
|
|
574
|
+
const envValue = await password({
|
|
575
|
+
message: `Value for ${envName.trim()} (stored locally, never sent to servers):`,
|
|
576
|
+
mask: "*"
|
|
577
|
+
});
|
|
578
|
+
envPairs[envName.trim()] = envValue;
|
|
579
|
+
addMore = true;
|
|
580
|
+
}
|
|
581
|
+
const config = {
|
|
582
|
+
transport: "stdio",
|
|
583
|
+
command: command.trim(),
|
|
584
|
+
args,
|
|
585
|
+
env: envPairs
|
|
586
|
+
};
|
|
587
|
+
const definition = {
|
|
588
|
+
name: name.trim(),
|
|
589
|
+
source: source.trim(),
|
|
590
|
+
default: config,
|
|
591
|
+
overrides: {}
|
|
592
|
+
};
|
|
593
|
+
await writeServerDefinition(definition);
|
|
594
|
+
console.log(`Server "${name.trim()}" saved to central repository.`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/commands/server-remove.ts
|
|
598
|
+
async function serverRemoveCommand(name) {
|
|
599
|
+
if (!serverExists(name)) {
|
|
600
|
+
console.error(`Error: Server "${name}" does not exist in central repository.`);
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
await removeServerDefinition(name);
|
|
605
|
+
console.log(`Server "${name}" removed from central repository.`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/commands/server-list.ts
|
|
609
|
+
async function serverListCommand() {
|
|
610
|
+
const servers = await listServerDefinitions();
|
|
611
|
+
if (servers.length === 0) {
|
|
612
|
+
console.log('No servers in central repository. Use "mcpsmgr server add" to add one.');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
console.log("\nCentral Repository Servers:\n");
|
|
616
|
+
for (const server2 of servers) {
|
|
617
|
+
const overrideCount = Object.keys(server2.overrides).length;
|
|
618
|
+
const overrideInfo = overrideCount > 0 ? ` (${overrideCount} overrides)` : "";
|
|
619
|
+
console.log(
|
|
620
|
+
` ${server2.name} [${server2.default.transport}]${overrideInfo}`
|
|
621
|
+
);
|
|
622
|
+
if (server2.source) {
|
|
623
|
+
console.log(` source: ${server2.source}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
console.log();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/commands/init.ts
|
|
630
|
+
import { checkbox, confirm as confirm3 } from "@inquirer/prompts";
|
|
631
|
+
|
|
632
|
+
// src/adapters/index.ts
|
|
633
|
+
import { existsSync as existsSync6 } from "fs";
|
|
634
|
+
|
|
635
|
+
// src/adapters/claude-code.ts
|
|
636
|
+
import { join as join2 } from "path";
|
|
637
|
+
|
|
638
|
+
// src/adapters/json-file.ts
|
|
639
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
640
|
+
import { existsSync as existsSync4 } from "fs";
|
|
641
|
+
import { dirname } from "path";
|
|
642
|
+
async function readJsonFile(filePath) {
|
|
643
|
+
if (!existsSync4(filePath)) {
|
|
644
|
+
return {};
|
|
645
|
+
}
|
|
646
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
647
|
+
return JSON.parse(raw);
|
|
648
|
+
}
|
|
649
|
+
async function writeJsonFile(filePath, data) {
|
|
650
|
+
const dir = dirname(filePath);
|
|
651
|
+
if (!existsSync4(dir)) {
|
|
652
|
+
await mkdir4(dir, { recursive: true });
|
|
653
|
+
}
|
|
654
|
+
await writeFile3(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/adapters/env-args.ts
|
|
658
|
+
var ENV_VAR_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*=/;
|
|
659
|
+
function buildEnvArgs(env) {
|
|
660
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
661
|
+
}
|
|
662
|
+
function parseEnvArgs(args) {
|
|
663
|
+
const env = {};
|
|
664
|
+
for (let i = 0; i < args.length; i++) {
|
|
665
|
+
if (ENV_VAR_PATTERN.test(args[i])) {
|
|
666
|
+
const eqIndex = args[i].indexOf("=");
|
|
667
|
+
env[args[i].slice(0, eqIndex)] = args[i].slice(eqIndex + 1);
|
|
668
|
+
} else {
|
|
669
|
+
return { env, commandIndex: i };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { env, commandIndex: args.length };
|
|
673
|
+
}
|
|
674
|
+
function resolveEnvInArgs(args, env) {
|
|
675
|
+
const substitutedKeys = /* @__PURE__ */ new Set();
|
|
676
|
+
const resolvedArgs = args.map(
|
|
677
|
+
(arg) => arg.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
678
|
+
if (varName in env) {
|
|
679
|
+
substitutedKeys.add(varName);
|
|
680
|
+
return env[varName];
|
|
681
|
+
}
|
|
682
|
+
return match;
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
const remainingEnv = {};
|
|
686
|
+
for (const [key, value] of Object.entries(env)) {
|
|
687
|
+
if (!substitutedKeys.has(key)) {
|
|
688
|
+
remainingEnv[key] = value;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return { resolvedArgs, remainingEnv };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/adapters/claude-code.ts
|
|
695
|
+
function toAgentFormat(config) {
|
|
696
|
+
if (config.transport === "stdio") {
|
|
697
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
698
|
+
config.args,
|
|
699
|
+
config.env
|
|
700
|
+
);
|
|
701
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
702
|
+
if (envArgs.length > 0) {
|
|
703
|
+
return {
|
|
704
|
+
type: "stdio",
|
|
705
|
+
command: "env",
|
|
706
|
+
args: [...envArgs, config.command, ...resolvedArgs]
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
type: "stdio",
|
|
711
|
+
command: config.command,
|
|
712
|
+
args: resolvedArgs
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
type: "http",
|
|
717
|
+
url: config.url,
|
|
718
|
+
headers: { ...config.headers }
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function fromAgentFormat(_name, raw) {
|
|
722
|
+
const type = raw["type"];
|
|
723
|
+
if (type === "stdio") {
|
|
724
|
+
const command = raw["command"];
|
|
725
|
+
const rawArgs = raw["args"] ?? [];
|
|
726
|
+
const legacyEnv = raw["env"];
|
|
727
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
728
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
729
|
+
}
|
|
730
|
+
if (command === "env") {
|
|
731
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
732
|
+
return {
|
|
733
|
+
transport: "stdio",
|
|
734
|
+
command: rawArgs[commandIndex] ?? "",
|
|
735
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
736
|
+
env
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
740
|
+
}
|
|
741
|
+
if (type === "http") {
|
|
742
|
+
return {
|
|
743
|
+
transport: "http",
|
|
744
|
+
url: raw["url"],
|
|
745
|
+
headers: raw["headers"] ?? {}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
var claudeCodeAdapter = {
|
|
751
|
+
id: "claude-code",
|
|
752
|
+
name: "Claude Code",
|
|
753
|
+
configPath: (projectDir) => join2(projectDir, ".mcp.json"),
|
|
754
|
+
isGlobal: false,
|
|
755
|
+
toAgentFormat,
|
|
756
|
+
fromAgentFormat,
|
|
757
|
+
async read(projectDir) {
|
|
758
|
+
const filePath = join2(projectDir, ".mcp.json");
|
|
759
|
+
const data = await readJsonFile(filePath);
|
|
760
|
+
return data["mcpServers"] ?? {};
|
|
761
|
+
},
|
|
762
|
+
async write(projectDir, serverName, config) {
|
|
763
|
+
const filePath = join2(projectDir, ".mcp.json");
|
|
764
|
+
const data = await readJsonFile(filePath);
|
|
765
|
+
const servers = data["mcpServers"] ?? {};
|
|
766
|
+
if (serverName in servers) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Conflict: "${serverName}" already exists in Claude Code config`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const updated = {
|
|
772
|
+
...data,
|
|
773
|
+
mcpServers: { ...servers, [serverName]: toAgentFormat(config) }
|
|
774
|
+
};
|
|
775
|
+
await writeJsonFile(filePath, updated);
|
|
776
|
+
},
|
|
777
|
+
async remove(projectDir, serverName) {
|
|
778
|
+
const filePath = join2(projectDir, ".mcp.json");
|
|
779
|
+
const data = await readJsonFile(filePath);
|
|
780
|
+
const servers = data["mcpServers"] ?? {};
|
|
781
|
+
const { [serverName]: _, ...rest } = servers;
|
|
782
|
+
await writeJsonFile(filePath, { ...data, mcpServers: rest });
|
|
783
|
+
},
|
|
784
|
+
async has(projectDir, serverName) {
|
|
785
|
+
const filePath = join2(projectDir, ".mcp.json");
|
|
786
|
+
const data = await readJsonFile(filePath);
|
|
787
|
+
const servers = data["mcpServers"] ?? {};
|
|
788
|
+
return serverName in servers;
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// src/adapters/codex-cli.ts
|
|
793
|
+
import { join as join3 } from "path";
|
|
794
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
|
|
795
|
+
import { existsSync as existsSync5 } from "fs";
|
|
796
|
+
import { dirname as dirname2 } from "path";
|
|
797
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
798
|
+
function toAgentFormat2(config) {
|
|
799
|
+
if (config.transport === "stdio") {
|
|
800
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
801
|
+
config.args,
|
|
802
|
+
config.env
|
|
803
|
+
);
|
|
804
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
805
|
+
if (envArgs.length > 0) {
|
|
806
|
+
return {
|
|
807
|
+
command: "env",
|
|
808
|
+
args: [...envArgs, config.command, ...resolvedArgs]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
command: config.command,
|
|
813
|
+
args: resolvedArgs
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
url: config.url,
|
|
818
|
+
headers: { ...config.headers }
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function fromAgentFormat2(_name, raw) {
|
|
822
|
+
if (raw["command"]) {
|
|
823
|
+
const command = raw["command"];
|
|
824
|
+
const rawArgs = raw["args"] ?? [];
|
|
825
|
+
const legacyEnv = raw["env"];
|
|
826
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
827
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
828
|
+
}
|
|
829
|
+
if (command === "env") {
|
|
830
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
831
|
+
return {
|
|
832
|
+
transport: "stdio",
|
|
833
|
+
command: rawArgs[commandIndex] ?? "",
|
|
834
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
835
|
+
env
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
839
|
+
}
|
|
840
|
+
if (raw["url"]) {
|
|
841
|
+
return {
|
|
842
|
+
transport: "http",
|
|
843
|
+
url: raw["url"],
|
|
844
|
+
headers: raw["headers"] ?? {}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return void 0;
|
|
848
|
+
}
|
|
849
|
+
async function readTomlFile(filePath) {
|
|
850
|
+
if (!existsSync5(filePath)) {
|
|
851
|
+
return { raw: "", parsed: {} };
|
|
852
|
+
}
|
|
853
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
854
|
+
const parsed = parseToml(raw);
|
|
855
|
+
return { raw, parsed };
|
|
856
|
+
}
|
|
857
|
+
async function writeTomlFile(filePath, data) {
|
|
858
|
+
const dir = dirname2(filePath);
|
|
859
|
+
if (!existsSync5(dir)) {
|
|
860
|
+
await mkdir5(dir, { recursive: true });
|
|
861
|
+
}
|
|
862
|
+
await writeFile4(filePath, stringifyToml(data) + "\n", "utf-8");
|
|
863
|
+
}
|
|
864
|
+
var codexCliAdapter = {
|
|
865
|
+
id: "codex-cli",
|
|
866
|
+
name: "Codex CLI",
|
|
867
|
+
configPath: (projectDir) => join3(projectDir, ".codex", "config.toml"),
|
|
868
|
+
isGlobal: false,
|
|
869
|
+
toAgentFormat: toAgentFormat2,
|
|
870
|
+
fromAgentFormat: fromAgentFormat2,
|
|
871
|
+
async read(projectDir) {
|
|
872
|
+
const filePath = join3(projectDir, ".codex", "config.toml");
|
|
873
|
+
const { parsed } = await readTomlFile(filePath);
|
|
874
|
+
return parsed["mcp_servers"] ?? {};
|
|
875
|
+
},
|
|
876
|
+
async write(projectDir, serverName, config) {
|
|
877
|
+
const filePath = join3(projectDir, ".codex", "config.toml");
|
|
878
|
+
const { parsed } = await readTomlFile(filePath);
|
|
879
|
+
const servers = parsed["mcp_servers"] ?? {};
|
|
880
|
+
if (serverName in servers) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Conflict: "${serverName}" already exists in Codex CLI config`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const updated = {
|
|
886
|
+
...parsed,
|
|
887
|
+
mcp_servers: { ...servers, [serverName]: toAgentFormat2(config) }
|
|
888
|
+
};
|
|
889
|
+
await writeTomlFile(filePath, updated);
|
|
890
|
+
},
|
|
891
|
+
async remove(projectDir, serverName) {
|
|
892
|
+
const filePath = join3(projectDir, ".codex", "config.toml");
|
|
893
|
+
const { parsed } = await readTomlFile(filePath);
|
|
894
|
+
const servers = parsed["mcp_servers"] ?? {};
|
|
895
|
+
const { [serverName]: _, ...rest } = servers;
|
|
896
|
+
await writeTomlFile(filePath, { ...parsed, mcp_servers: rest });
|
|
897
|
+
},
|
|
898
|
+
async has(projectDir, serverName) {
|
|
899
|
+
const filePath = join3(projectDir, ".codex", "config.toml");
|
|
900
|
+
const { parsed } = await readTomlFile(filePath);
|
|
901
|
+
const servers = parsed["mcp_servers"] ?? {};
|
|
902
|
+
return serverName in servers;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// src/adapters/gemini-cli.ts
|
|
907
|
+
import { join as join4 } from "path";
|
|
908
|
+
function toAgentFormat3(config) {
|
|
909
|
+
if (config.transport === "stdio") {
|
|
910
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
911
|
+
config.args,
|
|
912
|
+
config.env
|
|
913
|
+
);
|
|
914
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
915
|
+
if (envArgs.length > 0) {
|
|
916
|
+
return {
|
|
917
|
+
command: "env",
|
|
918
|
+
args: [...envArgs, config.command, ...resolvedArgs]
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
command: config.command,
|
|
923
|
+
args: resolvedArgs
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
url: config.url,
|
|
928
|
+
headers: { ...config.headers }
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function fromAgentFormat3(_name, raw) {
|
|
932
|
+
if (raw["command"]) {
|
|
933
|
+
const command = raw["command"];
|
|
934
|
+
const rawArgs = raw["args"] ?? [];
|
|
935
|
+
const legacyEnv = raw["env"];
|
|
936
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
937
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
938
|
+
}
|
|
939
|
+
if (command === "env") {
|
|
940
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
941
|
+
return {
|
|
942
|
+
transport: "stdio",
|
|
943
|
+
command: rawArgs[commandIndex] ?? "",
|
|
944
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
945
|
+
env
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
949
|
+
}
|
|
950
|
+
if (raw["url"]) {
|
|
951
|
+
return {
|
|
952
|
+
transport: "http",
|
|
953
|
+
url: raw["url"],
|
|
954
|
+
headers: raw["headers"] ?? {}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return void 0;
|
|
958
|
+
}
|
|
959
|
+
var geminiCliAdapter = {
|
|
960
|
+
id: "gemini-cli",
|
|
961
|
+
name: "Gemini CLI",
|
|
962
|
+
configPath: (projectDir) => join4(projectDir, ".gemini", "settings.json"),
|
|
963
|
+
isGlobal: false,
|
|
964
|
+
toAgentFormat: toAgentFormat3,
|
|
965
|
+
fromAgentFormat: fromAgentFormat3,
|
|
966
|
+
async read(projectDir) {
|
|
967
|
+
const filePath = join4(projectDir, ".gemini", "settings.json");
|
|
968
|
+
const data = await readJsonFile(filePath);
|
|
969
|
+
return data["mcpServers"] ?? {};
|
|
970
|
+
},
|
|
971
|
+
async write(projectDir, serverName, config) {
|
|
972
|
+
const filePath = join4(projectDir, ".gemini", "settings.json");
|
|
973
|
+
const data = await readJsonFile(filePath);
|
|
974
|
+
const servers = data["mcpServers"] ?? {};
|
|
975
|
+
if (serverName in servers) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`Conflict: "${serverName}" already exists in Gemini CLI config`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const updated = {
|
|
981
|
+
...data,
|
|
982
|
+
mcpServers: { ...servers, [serverName]: toAgentFormat3(config) }
|
|
983
|
+
};
|
|
984
|
+
await writeJsonFile(filePath, updated);
|
|
985
|
+
},
|
|
986
|
+
async remove(projectDir, serverName) {
|
|
987
|
+
const filePath = join4(projectDir, ".gemini", "settings.json");
|
|
988
|
+
const data = await readJsonFile(filePath);
|
|
989
|
+
const servers = data["mcpServers"] ?? {};
|
|
990
|
+
const { [serverName]: _, ...rest } = servers;
|
|
991
|
+
await writeJsonFile(filePath, { ...data, mcpServers: rest });
|
|
992
|
+
},
|
|
993
|
+
async has(projectDir, serverName) {
|
|
994
|
+
const filePath = join4(projectDir, ".gemini", "settings.json");
|
|
995
|
+
const data = await readJsonFile(filePath);
|
|
996
|
+
const servers = data["mcpServers"] ?? {};
|
|
997
|
+
return serverName in servers;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/adapters/opencode.ts
|
|
1002
|
+
import { join as join5 } from "path";
|
|
1003
|
+
function toAgentFormat4(config) {
|
|
1004
|
+
if (config.transport === "stdio") {
|
|
1005
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
1006
|
+
config.args,
|
|
1007
|
+
config.env
|
|
1008
|
+
);
|
|
1009
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
1010
|
+
if (envArgs.length > 0) {
|
|
1011
|
+
return {
|
|
1012
|
+
type: "local",
|
|
1013
|
+
command: ["env", ...envArgs, config.command, ...resolvedArgs]
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
type: "local",
|
|
1018
|
+
command: [config.command, ...resolvedArgs]
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
type: "remote",
|
|
1023
|
+
url: config.url,
|
|
1024
|
+
headers: { ...config.headers }
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function fromAgentFormat4(_name, raw) {
|
|
1028
|
+
const type = raw["type"];
|
|
1029
|
+
if (type === "local") {
|
|
1030
|
+
const commandArr = raw["command"];
|
|
1031
|
+
const legacyEnv = raw["environment"];
|
|
1032
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
1033
|
+
const [command2 = "", ...args2] = commandArr;
|
|
1034
|
+
return { transport: "stdio", command: command2, args: args2, env: legacyEnv };
|
|
1035
|
+
}
|
|
1036
|
+
if (commandArr[0] === "env") {
|
|
1037
|
+
const { env, commandIndex } = parseEnvArgs(commandArr.slice(1));
|
|
1038
|
+
const actualIndex = commandIndex + 1;
|
|
1039
|
+
return {
|
|
1040
|
+
transport: "stdio",
|
|
1041
|
+
command: commandArr[actualIndex] ?? "",
|
|
1042
|
+
args: commandArr.slice(actualIndex + 1),
|
|
1043
|
+
env
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
const [command = "", ...args] = commandArr;
|
|
1047
|
+
return { transport: "stdio", command, args, env: {} };
|
|
1048
|
+
}
|
|
1049
|
+
if (type === "remote") {
|
|
1050
|
+
return {
|
|
1051
|
+
transport: "http",
|
|
1052
|
+
url: raw["url"],
|
|
1053
|
+
headers: raw["headers"] ?? {}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return void 0;
|
|
1057
|
+
}
|
|
1058
|
+
var opencodeAdapter = {
|
|
1059
|
+
id: "opencode",
|
|
1060
|
+
name: "OpenCode",
|
|
1061
|
+
configPath: (projectDir) => join5(projectDir, "opencode.json"),
|
|
1062
|
+
isGlobal: false,
|
|
1063
|
+
toAgentFormat: toAgentFormat4,
|
|
1064
|
+
fromAgentFormat: fromAgentFormat4,
|
|
1065
|
+
async read(projectDir) {
|
|
1066
|
+
const filePath = join5(projectDir, "opencode.json");
|
|
1067
|
+
const data = await readJsonFile(filePath);
|
|
1068
|
+
return data["mcp"] ?? {};
|
|
1069
|
+
},
|
|
1070
|
+
async write(projectDir, serverName, config) {
|
|
1071
|
+
const filePath = join5(projectDir, "opencode.json");
|
|
1072
|
+
const data = await readJsonFile(filePath);
|
|
1073
|
+
const servers = data["mcp"] ?? {};
|
|
1074
|
+
if (serverName in servers) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Conflict: "${serverName}" already exists in OpenCode config`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const updated = {
|
|
1080
|
+
...data,
|
|
1081
|
+
mcp: { ...servers, [serverName]: toAgentFormat4(config) }
|
|
1082
|
+
};
|
|
1083
|
+
await writeJsonFile(filePath, updated);
|
|
1084
|
+
},
|
|
1085
|
+
async remove(projectDir, serverName) {
|
|
1086
|
+
const filePath = join5(projectDir, "opencode.json");
|
|
1087
|
+
const data = await readJsonFile(filePath);
|
|
1088
|
+
const servers = data["mcp"] ?? {};
|
|
1089
|
+
const { [serverName]: _, ...rest } = servers;
|
|
1090
|
+
await writeJsonFile(filePath, { ...data, mcp: rest });
|
|
1091
|
+
},
|
|
1092
|
+
async has(projectDir, serverName) {
|
|
1093
|
+
const filePath = join5(projectDir, "opencode.json");
|
|
1094
|
+
const data = await readJsonFile(filePath);
|
|
1095
|
+
const servers = data["mcp"] ?? {};
|
|
1096
|
+
return serverName in servers;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/adapters/antigravity.ts
|
|
1101
|
+
import { homedir as homedir2 } from "os";
|
|
1102
|
+
import { join as join6 } from "path";
|
|
1103
|
+
var GLOBAL_CONFIG_PATH = join6(
|
|
1104
|
+
homedir2(),
|
|
1105
|
+
".gemini",
|
|
1106
|
+
"antigravity",
|
|
1107
|
+
"mcp_config.json"
|
|
1108
|
+
);
|
|
1109
|
+
function toAgentFormat5(config) {
|
|
1110
|
+
if (config.transport === "stdio") {
|
|
1111
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
1112
|
+
config.args,
|
|
1113
|
+
config.env
|
|
1114
|
+
);
|
|
1115
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
1116
|
+
if (envArgs.length > 0) {
|
|
1117
|
+
return {
|
|
1118
|
+
command: "env",
|
|
1119
|
+
args: [...envArgs, config.command, ...resolvedArgs]
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
command: config.command,
|
|
1124
|
+
args: resolvedArgs
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
serverUrl: config.url,
|
|
1129
|
+
headers: { ...config.headers }
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function fromAgentFormat5(_name, raw) {
|
|
1133
|
+
if (raw["command"]) {
|
|
1134
|
+
const command = raw["command"];
|
|
1135
|
+
const rawArgs = raw["args"] ?? [];
|
|
1136
|
+
const legacyEnv = raw["env"];
|
|
1137
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
1138
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
1139
|
+
}
|
|
1140
|
+
if (command === "env") {
|
|
1141
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
1142
|
+
return {
|
|
1143
|
+
transport: "stdio",
|
|
1144
|
+
command: rawArgs[commandIndex] ?? "",
|
|
1145
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
1146
|
+
env
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
1150
|
+
}
|
|
1151
|
+
if (raw["serverUrl"]) {
|
|
1152
|
+
return {
|
|
1153
|
+
transport: "http",
|
|
1154
|
+
url: raw["serverUrl"],
|
|
1155
|
+
headers: raw["headers"] ?? {}
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return void 0;
|
|
1159
|
+
}
|
|
1160
|
+
var antigravityAdapter = {
|
|
1161
|
+
id: "antigravity",
|
|
1162
|
+
name: "Antigravity",
|
|
1163
|
+
configPath: () => GLOBAL_CONFIG_PATH,
|
|
1164
|
+
isGlobal: true,
|
|
1165
|
+
toAgentFormat: toAgentFormat5,
|
|
1166
|
+
fromAgentFormat: fromAgentFormat5,
|
|
1167
|
+
async read() {
|
|
1168
|
+
const data = await readJsonFile(GLOBAL_CONFIG_PATH);
|
|
1169
|
+
return data["mcpServers"] ?? {};
|
|
1170
|
+
},
|
|
1171
|
+
async write(_projectDir, serverName, config) {
|
|
1172
|
+
const data = await readJsonFile(GLOBAL_CONFIG_PATH);
|
|
1173
|
+
const servers = data["mcpServers"] ?? {};
|
|
1174
|
+
if (serverName in servers) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Conflict: "${serverName}" already exists in Antigravity config`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const updated = {
|
|
1180
|
+
...data,
|
|
1181
|
+
mcpServers: { ...servers, [serverName]: toAgentFormat5(config) }
|
|
1182
|
+
};
|
|
1183
|
+
await writeJsonFile(GLOBAL_CONFIG_PATH, updated);
|
|
1184
|
+
},
|
|
1185
|
+
async remove(_projectDir, serverName) {
|
|
1186
|
+
const data = await readJsonFile(GLOBAL_CONFIG_PATH);
|
|
1187
|
+
const servers = data["mcpServers"] ?? {};
|
|
1188
|
+
const { [serverName]: _, ...rest } = servers;
|
|
1189
|
+
await writeJsonFile(GLOBAL_CONFIG_PATH, { ...data, mcpServers: rest });
|
|
1190
|
+
},
|
|
1191
|
+
async has(_projectDir, serverName) {
|
|
1192
|
+
const data = await readJsonFile(GLOBAL_CONFIG_PATH);
|
|
1193
|
+
const servers = data["mcpServers"] ?? {};
|
|
1194
|
+
return serverName in servers;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// src/adapters/index.ts
|
|
1199
|
+
var allAdapters = [
|
|
1200
|
+
claudeCodeAdapter,
|
|
1201
|
+
codexCliAdapter,
|
|
1202
|
+
geminiCliAdapter,
|
|
1203
|
+
opencodeAdapter,
|
|
1204
|
+
antigravityAdapter
|
|
1205
|
+
];
|
|
1206
|
+
function detectAgents(projectDir) {
|
|
1207
|
+
return allAdapters.filter((adapter) => {
|
|
1208
|
+
if (adapter.isGlobal) {
|
|
1209
|
+
return existsSync6(adapter.configPath(projectDir));
|
|
1210
|
+
}
|
|
1211
|
+
return existsSync6(adapter.configPath(projectDir));
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/utils/resolve-config.ts
|
|
1216
|
+
function resolveConfig(definition, adapter) {
|
|
1217
|
+
const override = definition.overrides[adapter.id];
|
|
1218
|
+
if (override) {
|
|
1219
|
+
const base = definition.default;
|
|
1220
|
+
return { ...base, ...override };
|
|
1221
|
+
}
|
|
1222
|
+
return definition.default;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/commands/init.ts
|
|
1226
|
+
async function initCommand() {
|
|
1227
|
+
try {
|
|
1228
|
+
await initCommandInner();
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (isUserCancellation(error)) return;
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async function initCommandInner() {
|
|
1235
|
+
const projectDir = process.cwd();
|
|
1236
|
+
const servers = await listServerDefinitions();
|
|
1237
|
+
if (servers.length === 0) {
|
|
1238
|
+
console.log(
|
|
1239
|
+
'Central repository is empty. Use "mcpsmgr server add" to add servers first.'
|
|
1240
|
+
);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const detected = detectAgents(projectDir);
|
|
1244
|
+
const detectedIds = new Set(detected.map((a) => a.id));
|
|
1245
|
+
const selectedAgents = await checkbox({
|
|
1246
|
+
message: "Select agents to configure:",
|
|
1247
|
+
choices: allAdapters.map((adapter) => ({
|
|
1248
|
+
name: `${adapter.name}${detectedIds.has(adapter.id) ? " (detected)" : ""}${adapter.isGlobal ? " [global]" : ""}`,
|
|
1249
|
+
value: adapter,
|
|
1250
|
+
checked: detectedIds.has(adapter.id) && !adapter.isGlobal
|
|
1251
|
+
}))
|
|
1252
|
+
});
|
|
1253
|
+
if (selectedAgents.length === 0) {
|
|
1254
|
+
console.log("No agents selected.");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const agentServerMap = /* @__PURE__ */ new Map();
|
|
1258
|
+
const detectedServers = /* @__PURE__ */ new Set();
|
|
1259
|
+
for (const agent of selectedAgents) {
|
|
1260
|
+
try {
|
|
1261
|
+
const existing = await agent.read(projectDir);
|
|
1262
|
+
const names = new Set(Object.keys(existing));
|
|
1263
|
+
agentServerMap.set(agent.id, names);
|
|
1264
|
+
for (const name of names) {
|
|
1265
|
+
detectedServers.add(name);
|
|
1266
|
+
}
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const selectedServers = await checkbox({
|
|
1271
|
+
message: "Select servers to deploy:",
|
|
1272
|
+
choices: servers.map((s) => ({
|
|
1273
|
+
name: `${s.name}${detectedServers.has(s.name) ? " (detected)" : ""} [${s.default.transport}]`,
|
|
1274
|
+
value: s,
|
|
1275
|
+
checked: detectedServers.has(s.name)
|
|
1276
|
+
}))
|
|
1277
|
+
});
|
|
1278
|
+
const selectedServerNames = new Set(selectedServers.map((s) => s.name));
|
|
1279
|
+
const removals = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const serverName of detectedServers) {
|
|
1281
|
+
if (!selectedServerNames.has(serverName)) {
|
|
1282
|
+
const agents = selectedAgents.filter((a) => {
|
|
1283
|
+
const agentServers = agentServerMap.get(a.id);
|
|
1284
|
+
return agentServers?.has(serverName);
|
|
1285
|
+
});
|
|
1286
|
+
if (agents.length > 0) {
|
|
1287
|
+
removals.set(serverName, agents);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (selectedServers.length === 0 && removals.size === 0) {
|
|
1292
|
+
console.log("No servers selected.");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
console.log("\nPlan:");
|
|
1296
|
+
for (const agent of selectedAgents) {
|
|
1297
|
+
console.log(` ${agent.name}:`);
|
|
1298
|
+
for (const server2 of selectedServers) {
|
|
1299
|
+
console.log(` + ${server2.name}`);
|
|
1300
|
+
}
|
|
1301
|
+
for (const [serverName, agents] of removals) {
|
|
1302
|
+
if (agents.some((a) => a.id === agent.id)) {
|
|
1303
|
+
console.log(` - ${serverName}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const proceed = await confirm3({ message: "Proceed?" });
|
|
1308
|
+
if (!proceed) {
|
|
1309
|
+
console.log("Cancelled.");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
for (const agent of selectedAgents) {
|
|
1313
|
+
for (const server2 of selectedServers) {
|
|
1314
|
+
try {
|
|
1315
|
+
const config = resolveConfig(server2, agent);
|
|
1316
|
+
await agent.write(projectDir, server2.name, config);
|
|
1317
|
+
console.log(` + ${server2.name} -> ${agent.name}`);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
console.warn(
|
|
1320
|
+
` ! ${server2.name} -> ${agent.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
for (const [serverName, agents] of removals) {
|
|
1326
|
+
for (const agent of agents) {
|
|
1327
|
+
try {
|
|
1328
|
+
await agent.remove(projectDir, serverName);
|
|
1329
|
+
console.log(` - ${serverName} <- ${agent.name}`);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
console.warn(
|
|
1332
|
+
` ! ${serverName} <- ${agent.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
console.log("\nDone.");
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/commands/add.ts
|
|
1341
|
+
import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
1342
|
+
async function addCommand(serverName) {
|
|
1343
|
+
try {
|
|
1344
|
+
await addCommandInner(serverName);
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
if (isUserCancellation(error)) return;
|
|
1347
|
+
throw error;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async function addCommandInner(serverName) {
|
|
1351
|
+
const projectDir = process.cwd();
|
|
1352
|
+
if (!serverExists(serverName)) {
|
|
1353
|
+
console.error(
|
|
1354
|
+
`Error: Server "${serverName}" not found in central repository.`
|
|
1355
|
+
);
|
|
1356
|
+
process.exitCode = 1;
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const definition = await readServerDefinition(serverName);
|
|
1360
|
+
if (!definition) {
|
|
1361
|
+
console.error(`Error: Failed to read server definition for "${serverName}".`);
|
|
1362
|
+
process.exitCode = 1;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const detected = detectAgents(projectDir);
|
|
1366
|
+
if (detected.length === 0) {
|
|
1367
|
+
console.log(
|
|
1368
|
+
'No agent config files detected in this project. Use "mcpsmgr init" first.'
|
|
1369
|
+
);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const selectedAgents = await checkbox2({
|
|
1373
|
+
message: `Select agents to add "${serverName}" to:`,
|
|
1374
|
+
choices: detected.map((adapter) => ({
|
|
1375
|
+
name: `${adapter.name}${adapter.isGlobal ? " [global]" : ""}`,
|
|
1376
|
+
value: adapter,
|
|
1377
|
+
checked: !adapter.isGlobal
|
|
1378
|
+
}))
|
|
1379
|
+
});
|
|
1380
|
+
if (selectedAgents.length === 0) {
|
|
1381
|
+
console.log("No agents selected.");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
for (const agent of selectedAgents) {
|
|
1385
|
+
try {
|
|
1386
|
+
const config = resolveConfig(definition, agent);
|
|
1387
|
+
await agent.write(projectDir, serverName, config);
|
|
1388
|
+
console.log(` + ${serverName} -> ${agent.name}`);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
console.warn(
|
|
1391
|
+
` ! ${serverName} -> ${agent.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/commands/remove.ts
|
|
1398
|
+
import { checkbox as checkbox3 } from "@inquirer/prompts";
|
|
1399
|
+
async function removeCommand(serverName) {
|
|
1400
|
+
try {
|
|
1401
|
+
await removeCommandInner(serverName);
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
if (isUserCancellation(error)) return;
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async function removeCommandInner(serverName) {
|
|
1408
|
+
const projectDir = process.cwd();
|
|
1409
|
+
const agentsWithServer = [];
|
|
1410
|
+
for (const adapter of allAdapters) {
|
|
1411
|
+
try {
|
|
1412
|
+
const has = await adapter.has(projectDir, serverName);
|
|
1413
|
+
if (has) {
|
|
1414
|
+
agentsWithServer.push(adapter);
|
|
1415
|
+
}
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (agentsWithServer.length === 0) {
|
|
1420
|
+
console.log(
|
|
1421
|
+
`Server "${serverName}" not found in any agent configuration.`
|
|
1422
|
+
);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const selectedAgents = await checkbox3({
|
|
1426
|
+
message: `Remove "${serverName}" from which agents?`,
|
|
1427
|
+
choices: agentsWithServer.map((adapter) => ({
|
|
1428
|
+
name: `${adapter.name}${adapter.isGlobal ? " [global]" : ""}`,
|
|
1429
|
+
value: adapter,
|
|
1430
|
+
checked: !adapter.isGlobal
|
|
1431
|
+
}))
|
|
1432
|
+
});
|
|
1433
|
+
if (selectedAgents.length === 0) {
|
|
1434
|
+
console.log("No agents selected.");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
for (const agent of selectedAgents) {
|
|
1438
|
+
try {
|
|
1439
|
+
await agent.remove(projectDir, serverName);
|
|
1440
|
+
console.log(` - ${serverName} <- ${agent.name}`);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.warn(
|
|
1443
|
+
` ! ${serverName} <- ${agent.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/commands/sync.ts
|
|
1450
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1451
|
+
async function syncCommand() {
|
|
1452
|
+
try {
|
|
1453
|
+
await syncCommandInner();
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
if (isUserCancellation(error)) return;
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async function syncCommandInner() {
|
|
1460
|
+
const projectDir = process.cwd();
|
|
1461
|
+
const definitions = await listServerDefinitions();
|
|
1462
|
+
if (definitions.length === 0) {
|
|
1463
|
+
console.log("Central repository is empty. Nothing to sync.");
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const changes = [];
|
|
1467
|
+
for (const adapter of allAdapters) {
|
|
1468
|
+
let servers;
|
|
1469
|
+
try {
|
|
1470
|
+
servers = await adapter.read(projectDir);
|
|
1471
|
+
} catch {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
for (const definition of definitions) {
|
|
1475
|
+
if (!(definition.name in servers)) {
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
const currentRaw = servers[definition.name];
|
|
1479
|
+
const desired = resolveConfig(definition, adapter);
|
|
1480
|
+
const desiredRaw = adapter.toAgentFormat(desired);
|
|
1481
|
+
if (JSON.stringify(currentRaw) !== JSON.stringify(desiredRaw)) {
|
|
1482
|
+
changes.push({
|
|
1483
|
+
agentName: adapter.name,
|
|
1484
|
+
serverName: definition.name,
|
|
1485
|
+
action: "update"
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (changes.length === 0) {
|
|
1491
|
+
console.log("All agent configurations are up to date.");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
console.log("\nSync preview:");
|
|
1495
|
+
for (const change of changes) {
|
|
1496
|
+
console.log(` ~ ${change.serverName} -> ${change.agentName}`);
|
|
1497
|
+
}
|
|
1498
|
+
const proceed = await confirm4({ message: "Apply changes?" });
|
|
1499
|
+
if (!proceed) {
|
|
1500
|
+
console.log("Cancelled.");
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
for (const adapter of allAdapters) {
|
|
1504
|
+
for (const definition of definitions) {
|
|
1505
|
+
const relevant = changes.find(
|
|
1506
|
+
(c) => c.agentName === adapter.name && c.serverName === definition.name
|
|
1507
|
+
);
|
|
1508
|
+
if (!relevant) continue;
|
|
1509
|
+
try {
|
|
1510
|
+
await adapter.remove(projectDir, definition.name);
|
|
1511
|
+
const config = resolveConfig(definition, adapter);
|
|
1512
|
+
await adapter.write(projectDir, definition.name, config);
|
|
1513
|
+
console.log(` ~ ${definition.name} -> ${adapter.name} (updated)`);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
console.warn(
|
|
1516
|
+
` ! ${definition.name} -> ${adapter.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
console.log("\nSync complete.");
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/commands/list.ts
|
|
1525
|
+
async function listCommand() {
|
|
1526
|
+
const projectDir = process.cwd();
|
|
1527
|
+
const matrix = {};
|
|
1528
|
+
const activeAdapters = [];
|
|
1529
|
+
for (const adapter of allAdapters) {
|
|
1530
|
+
let servers;
|
|
1531
|
+
try {
|
|
1532
|
+
servers = await adapter.read(projectDir);
|
|
1533
|
+
} catch {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (Object.keys(servers).length === 0) continue;
|
|
1537
|
+
activeAdapters.push(adapter);
|
|
1538
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
1539
|
+
if (!matrix[name]) {
|
|
1540
|
+
matrix[name] = {};
|
|
1541
|
+
}
|
|
1542
|
+
const config = adapter.fromAgentFormat(
|
|
1543
|
+
name,
|
|
1544
|
+
raw
|
|
1545
|
+
);
|
|
1546
|
+
matrix[name][adapter.id] = {
|
|
1547
|
+
transport: config?.transport ?? "?"
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (Object.keys(matrix).length === 0) {
|
|
1552
|
+
console.log(
|
|
1553
|
+
'No MCP servers found in any agent configuration. Use "mcpsmgr init" to get started.'
|
|
1554
|
+
);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const serverNames = Object.keys(matrix).sort();
|
|
1558
|
+
const agentIds = activeAdapters.map((a) => a.id);
|
|
1559
|
+
const agentNames = activeAdapters.map((a) => a.name);
|
|
1560
|
+
const colWidths = [
|
|
1561
|
+
Math.max(6, ...serverNames.map((n) => n.length)),
|
|
1562
|
+
...agentNames.map((n) => Math.max(n.length, 5))
|
|
1563
|
+
];
|
|
1564
|
+
const header = [
|
|
1565
|
+
"Server".padEnd(colWidths.at(0) ?? 6),
|
|
1566
|
+
...agentNames.map((n, i) => n.padEnd(colWidths.at(i + 1) ?? 5))
|
|
1567
|
+
].join(" ");
|
|
1568
|
+
const separator = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
1569
|
+
console.log(`
|
|
1570
|
+
${header}`);
|
|
1571
|
+
console.log(separator);
|
|
1572
|
+
for (const name of serverNames) {
|
|
1573
|
+
const cells = [
|
|
1574
|
+
name.padEnd(colWidths.at(0) ?? 6),
|
|
1575
|
+
...agentIds.map((id, i) => {
|
|
1576
|
+
const entry = matrix[name][id];
|
|
1577
|
+
const val = entry ? entry.transport : "-";
|
|
1578
|
+
return val.padEnd(colWidths.at(i + 1) ?? 5);
|
|
1579
|
+
})
|
|
1580
|
+
];
|
|
1581
|
+
console.log(cells.join(" "));
|
|
1582
|
+
}
|
|
1583
|
+
console.log();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/index.ts
|
|
1587
|
+
function requireSetup() {
|
|
1588
|
+
if (!configExists()) {
|
|
1589
|
+
console.error(
|
|
1590
|
+
'mcpsmgr is not configured. Run "mcpsmgr setup" first.'
|
|
1591
|
+
);
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
program.name("mcpsmgr").description("Unified MCP server manager for multiple coding agents").version("0.1.0");
|
|
1596
|
+
program.command("setup").description("Initialize mcpsmgr configuration").action(setupCommand);
|
|
1597
|
+
var server = program.command("server").description("Manage MCP server definitions in central repository");
|
|
1598
|
+
server.command("add [source]").description("Add an MCP server (URL or GitHub owner/repo)").action((source) => {
|
|
1599
|
+
requireSetup();
|
|
1600
|
+
return serverAddCommand(source);
|
|
1601
|
+
});
|
|
1602
|
+
server.command("remove <name>").description("Remove an MCP server from central repository").action((name) => {
|
|
1603
|
+
requireSetup();
|
|
1604
|
+
return serverRemoveCommand(name);
|
|
1605
|
+
});
|
|
1606
|
+
server.command("list").description("List all servers in central repository").action(() => {
|
|
1607
|
+
requireSetup();
|
|
1608
|
+
return serverListCommand();
|
|
1609
|
+
});
|
|
1610
|
+
program.command("init").description("Initialize MCP servers for current project").action(() => {
|
|
1611
|
+
requireSetup();
|
|
1612
|
+
return initCommand();
|
|
1613
|
+
});
|
|
1614
|
+
program.command("add <server-name>").description("Add a server from central repository to current project").action((serverName) => {
|
|
1615
|
+
requireSetup();
|
|
1616
|
+
return addCommand(serverName);
|
|
1617
|
+
});
|
|
1618
|
+
program.command("remove <server-name>").description("Remove a server from current project agent configs").action((serverName) => {
|
|
1619
|
+
requireSetup();
|
|
1620
|
+
return removeCommand(serverName);
|
|
1621
|
+
});
|
|
1622
|
+
program.command("sync").description("Sync central repository changes to project agent configs").action(() => {
|
|
1623
|
+
requireSetup();
|
|
1624
|
+
return syncCommand();
|
|
1625
|
+
});
|
|
1626
|
+
program.command("list").description("List MCP servers across all agent configs in current project").action(() => {
|
|
1627
|
+
requireSetup();
|
|
1628
|
+
return listCommand();
|
|
1629
|
+
});
|
|
1630
|
+
program.parse();
|
|
1631
|
+
//# sourceMappingURL=index.js.map
|