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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6
|
+
import type { AgentAdapter, DefaultConfig } from "../types.js";
|
|
7
|
+
import { buildEnvArgs, parseEnvArgs, resolveEnvInArgs } from "./env-args.js";
|
|
8
|
+
|
|
9
|
+
function toAgentFormat(config: DefaultConfig): Record<string, unknown> {
|
|
10
|
+
if (config.transport === "stdio") {
|
|
11
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
12
|
+
config.args,
|
|
13
|
+
config.env,
|
|
14
|
+
);
|
|
15
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
16
|
+
if (envArgs.length > 0) {
|
|
17
|
+
return {
|
|
18
|
+
command: "env",
|
|
19
|
+
args: [...envArgs, config.command, ...resolvedArgs],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
command: config.command,
|
|
24
|
+
args: resolvedArgs,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
url: config.url,
|
|
29
|
+
headers: { ...config.headers },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromAgentFormat(
|
|
34
|
+
_name: string,
|
|
35
|
+
raw: Record<string, unknown>,
|
|
36
|
+
): DefaultConfig | undefined {
|
|
37
|
+
if (raw["command"]) {
|
|
38
|
+
const command = raw["command"] as string;
|
|
39
|
+
const rawArgs = (raw["args"] as string[]) ?? [];
|
|
40
|
+
const legacyEnv = raw["env"] as Record<string, string> | undefined;
|
|
41
|
+
|
|
42
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
43
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (command === "env") {
|
|
47
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
48
|
+
return {
|
|
49
|
+
transport: "stdio",
|
|
50
|
+
command: rawArgs[commandIndex] ?? "",
|
|
51
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
52
|
+
env,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
57
|
+
}
|
|
58
|
+
if (raw["url"]) {
|
|
59
|
+
return {
|
|
60
|
+
transport: "http",
|
|
61
|
+
url: raw["url"] as string,
|
|
62
|
+
headers: (raw["headers"] as Record<string, string>) ?? {},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function readTomlFile(
|
|
69
|
+
filePath: string,
|
|
70
|
+
): Promise<{ raw: string; parsed: Record<string, unknown> }> {
|
|
71
|
+
if (!existsSync(filePath)) {
|
|
72
|
+
return { raw: "", parsed: {} };
|
|
73
|
+
}
|
|
74
|
+
const raw = await readFile(filePath, "utf-8");
|
|
75
|
+
const parsed = parseToml(raw) as Record<string, unknown>;
|
|
76
|
+
return { raw, parsed };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function writeTomlFile(
|
|
80
|
+
filePath: string,
|
|
81
|
+
data: Record<string, unknown>,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const dir = dirname(filePath);
|
|
84
|
+
if (!existsSync(dir)) {
|
|
85
|
+
await mkdir(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
await writeFile(filePath, stringifyToml(data) + "\n", "utf-8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const codexCliAdapter: AgentAdapter = {
|
|
91
|
+
id: "codex-cli",
|
|
92
|
+
name: "Codex CLI",
|
|
93
|
+
configPath: (projectDir) => join(projectDir, ".codex", "config.toml"),
|
|
94
|
+
isGlobal: false,
|
|
95
|
+
|
|
96
|
+
toAgentFormat,
|
|
97
|
+
fromAgentFormat,
|
|
98
|
+
|
|
99
|
+
async read(projectDir) {
|
|
100
|
+
const filePath = join(projectDir, ".codex", "config.toml");
|
|
101
|
+
const { parsed } = await readTomlFile(filePath);
|
|
102
|
+
return (parsed["mcp_servers"] as Record<string, unknown>) ?? {};
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async write(projectDir, serverName, config) {
|
|
106
|
+
const filePath = join(projectDir, ".codex", "config.toml");
|
|
107
|
+
const { parsed } = await readTomlFile(filePath);
|
|
108
|
+
const servers = (parsed["mcp_servers"] as Record<string, unknown>) ?? {};
|
|
109
|
+
if (serverName in servers) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Conflict: "${serverName}" already exists in Codex CLI config`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const updated = {
|
|
115
|
+
...parsed,
|
|
116
|
+
mcp_servers: { ...servers, [serverName]: toAgentFormat(config) },
|
|
117
|
+
};
|
|
118
|
+
await writeTomlFile(filePath, updated);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async remove(projectDir, serverName) {
|
|
122
|
+
const filePath = join(projectDir, ".codex", "config.toml");
|
|
123
|
+
const { parsed } = await readTomlFile(filePath);
|
|
124
|
+
const servers = (parsed["mcp_servers"] as Record<string, unknown>) ?? {};
|
|
125
|
+
const { [serverName]: _, ...rest } = servers;
|
|
126
|
+
await writeTomlFile(filePath, { ...parsed, mcp_servers: rest });
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async has(projectDir, serverName) {
|
|
130
|
+
const filePath = join(projectDir, ".codex", "config.toml");
|
|
131
|
+
const { parsed } = await readTomlFile(filePath);
|
|
132
|
+
const servers = (parsed["mcp_servers"] as Record<string, unknown>) ?? {};
|
|
133
|
+
return serverName in servers;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const ENV_VAR_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*=/;
|
|
2
|
+
|
|
3
|
+
export function buildEnvArgs(
|
|
4
|
+
env: Readonly<Record<string, string>>,
|
|
5
|
+
): string[] {
|
|
6
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseEnvArgs(args: readonly string[]): {
|
|
10
|
+
readonly env: Record<string, string>;
|
|
11
|
+
readonly commandIndex: number;
|
|
12
|
+
} {
|
|
13
|
+
const env: Record<string, string> = {};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
if (ENV_VAR_PATTERN.test(args[i])) {
|
|
16
|
+
const eqIndex = args[i].indexOf("=");
|
|
17
|
+
env[args[i].slice(0, eqIndex)] = args[i].slice(eqIndex + 1);
|
|
18
|
+
} else {
|
|
19
|
+
return { env, commandIndex: i };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { env, commandIndex: args.length };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveEnvInArgs(
|
|
26
|
+
args: readonly string[],
|
|
27
|
+
env: Readonly<Record<string, string>>,
|
|
28
|
+
): {
|
|
29
|
+
readonly resolvedArgs: string[];
|
|
30
|
+
readonly remainingEnv: Record<string, string>;
|
|
31
|
+
} {
|
|
32
|
+
const substitutedKeys = new Set<string>();
|
|
33
|
+
const resolvedArgs = args.map((arg) =>
|
|
34
|
+
arg.replace(/\$\{([^}]+)\}/g, (match, varName: string) => {
|
|
35
|
+
if (varName in env) {
|
|
36
|
+
substitutedKeys.add(varName);
|
|
37
|
+
return env[varName];
|
|
38
|
+
}
|
|
39
|
+
return match;
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const remainingEnv: Record<string, string> = {};
|
|
44
|
+
for (const [key, value] of Object.entries(env)) {
|
|
45
|
+
if (!substitutedKeys.has(key)) {
|
|
46
|
+
remainingEnv[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { resolvedArgs, remainingEnv };
|
|
51
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AgentAdapter, DefaultConfig } from "../types.js";
|
|
3
|
+
import { readJsonFile, writeJsonFile } from "./json-file.js";
|
|
4
|
+
import { buildEnvArgs, parseEnvArgs, resolveEnvInArgs } from "./env-args.js";
|
|
5
|
+
|
|
6
|
+
function toAgentFormat(config: DefaultConfig): Record<string, unknown> {
|
|
7
|
+
if (config.transport === "stdio") {
|
|
8
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
9
|
+
config.args,
|
|
10
|
+
config.env,
|
|
11
|
+
);
|
|
12
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
13
|
+
if (envArgs.length > 0) {
|
|
14
|
+
return {
|
|
15
|
+
command: "env",
|
|
16
|
+
args: [...envArgs, config.command, ...resolvedArgs],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
command: config.command,
|
|
21
|
+
args: resolvedArgs,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
url: config.url,
|
|
26
|
+
headers: { ...config.headers },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fromAgentFormat(
|
|
31
|
+
_name: string,
|
|
32
|
+
raw: Record<string, unknown>,
|
|
33
|
+
): DefaultConfig | undefined {
|
|
34
|
+
if (raw["command"]) {
|
|
35
|
+
const command = raw["command"] as string;
|
|
36
|
+
const rawArgs = (raw["args"] as string[]) ?? [];
|
|
37
|
+
const legacyEnv = raw["env"] as Record<string, string> | undefined;
|
|
38
|
+
|
|
39
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
40
|
+
return { transport: "stdio", command, args: rawArgs, env: legacyEnv };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (command === "env") {
|
|
44
|
+
const { env, commandIndex } = parseEnvArgs(rawArgs);
|
|
45
|
+
return {
|
|
46
|
+
transport: "stdio",
|
|
47
|
+
command: rawArgs[commandIndex] ?? "",
|
|
48
|
+
args: rawArgs.slice(commandIndex + 1),
|
|
49
|
+
env,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { transport: "stdio", command, args: rawArgs, env: {} };
|
|
54
|
+
}
|
|
55
|
+
if (raw["url"]) {
|
|
56
|
+
return {
|
|
57
|
+
transport: "http",
|
|
58
|
+
url: raw["url"] as string,
|
|
59
|
+
headers: (raw["headers"] as Record<string, string>) ?? {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const geminiCliAdapter: AgentAdapter = {
|
|
66
|
+
id: "gemini-cli",
|
|
67
|
+
name: "Gemini CLI",
|
|
68
|
+
configPath: (projectDir) => join(projectDir, ".gemini", "settings.json"),
|
|
69
|
+
isGlobal: false,
|
|
70
|
+
|
|
71
|
+
toAgentFormat,
|
|
72
|
+
fromAgentFormat,
|
|
73
|
+
|
|
74
|
+
async read(projectDir) {
|
|
75
|
+
const filePath = join(projectDir, ".gemini", "settings.json");
|
|
76
|
+
const data = await readJsonFile(filePath);
|
|
77
|
+
return (data["mcpServers"] as Record<string, unknown>) ?? {};
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async write(projectDir, serverName, config) {
|
|
81
|
+
const filePath = join(projectDir, ".gemini", "settings.json");
|
|
82
|
+
const data = await readJsonFile(filePath);
|
|
83
|
+
const servers = (data["mcpServers"] as Record<string, unknown>) ?? {};
|
|
84
|
+
if (serverName in servers) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Conflict: "${serverName}" already exists in Gemini CLI config`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const updated = {
|
|
90
|
+
...data,
|
|
91
|
+
mcpServers: { ...servers, [serverName]: toAgentFormat(config) },
|
|
92
|
+
};
|
|
93
|
+
await writeJsonFile(filePath, updated);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async remove(projectDir, serverName) {
|
|
97
|
+
const filePath = join(projectDir, ".gemini", "settings.json");
|
|
98
|
+
const data = await readJsonFile(filePath);
|
|
99
|
+
const servers = (data["mcpServers"] as Record<string, unknown>) ?? {};
|
|
100
|
+
const { [serverName]: _, ...rest } = servers;
|
|
101
|
+
await writeJsonFile(filePath, { ...data, mcpServers: rest });
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async has(projectDir, serverName) {
|
|
105
|
+
const filePath = join(projectDir, ".gemini", "settings.json");
|
|
106
|
+
const data = await readJsonFile(filePath);
|
|
107
|
+
const servers = (data["mcpServers"] as Record<string, unknown>) ?? {};
|
|
108
|
+
return serverName in servers;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import type { AgentAdapter, AgentId } from "../types.js";
|
|
3
|
+
import { claudeCodeAdapter } from "./claude-code.js";
|
|
4
|
+
import { codexCliAdapter } from "./codex-cli.js";
|
|
5
|
+
import { geminiCliAdapter } from "./gemini-cli.js";
|
|
6
|
+
import { opencodeAdapter } from "./opencode.js";
|
|
7
|
+
import { antigravityAdapter } from "./antigravity.js";
|
|
8
|
+
|
|
9
|
+
export const allAdapters: readonly AgentAdapter[] = [
|
|
10
|
+
claudeCodeAdapter,
|
|
11
|
+
codexCliAdapter,
|
|
12
|
+
geminiCliAdapter,
|
|
13
|
+
opencodeAdapter,
|
|
14
|
+
antigravityAdapter,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function getAdapter(id: AgentId): AgentAdapter {
|
|
18
|
+
const adapter = allAdapters.find((a) => a.id === id);
|
|
19
|
+
if (!adapter) {
|
|
20
|
+
throw new Error(`Unknown agent: ${id}`);
|
|
21
|
+
}
|
|
22
|
+
return adapter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectAgents(projectDir: string): AgentAdapter[] {
|
|
26
|
+
return allAdapters.filter((adapter) => {
|
|
27
|
+
if (adapter.isGlobal) {
|
|
28
|
+
return existsSync(adapter.configPath(projectDir));
|
|
29
|
+
}
|
|
30
|
+
return existsSync(adapter.configPath(projectDir));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
export async function readJsonFile(
|
|
6
|
+
filePath: string,
|
|
7
|
+
): Promise<Record<string, unknown>> {
|
|
8
|
+
if (!existsSync(filePath)) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
const raw = await readFile(filePath, "utf-8");
|
|
12
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function writeJsonFile(
|
|
16
|
+
filePath: string,
|
|
17
|
+
data: Record<string, unknown>,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const dir = dirname(filePath);
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
await mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AgentAdapter, DefaultConfig } from "../types.js";
|
|
3
|
+
import { readJsonFile, writeJsonFile } from "./json-file.js";
|
|
4
|
+
import { buildEnvArgs, parseEnvArgs, resolveEnvInArgs } from "./env-args.js";
|
|
5
|
+
|
|
6
|
+
function toAgentFormat(config: DefaultConfig): Record<string, unknown> {
|
|
7
|
+
if (config.transport === "stdio") {
|
|
8
|
+
const { resolvedArgs, remainingEnv } = resolveEnvInArgs(
|
|
9
|
+
config.args,
|
|
10
|
+
config.env,
|
|
11
|
+
);
|
|
12
|
+
const envArgs = buildEnvArgs(remainingEnv);
|
|
13
|
+
if (envArgs.length > 0) {
|
|
14
|
+
return {
|
|
15
|
+
type: "local",
|
|
16
|
+
command: ["env", ...envArgs, config.command, ...resolvedArgs],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
type: "local",
|
|
21
|
+
command: [config.command, ...resolvedArgs],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
type: "remote",
|
|
26
|
+
url: config.url,
|
|
27
|
+
headers: { ...config.headers },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fromAgentFormat(
|
|
32
|
+
_name: string,
|
|
33
|
+
raw: Record<string, unknown>,
|
|
34
|
+
): DefaultConfig | undefined {
|
|
35
|
+
const type = raw["type"] as string | undefined;
|
|
36
|
+
if (type === "local") {
|
|
37
|
+
const commandArr = raw["command"] as string[];
|
|
38
|
+
const legacyEnv = raw["environment"] as Record<string, string> | undefined;
|
|
39
|
+
|
|
40
|
+
if (legacyEnv && Object.keys(legacyEnv).length > 0) {
|
|
41
|
+
const [command = "", ...args] = commandArr;
|
|
42
|
+
return { transport: "stdio", command, args, env: legacyEnv };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (commandArr[0] === "env") {
|
|
46
|
+
const { env, commandIndex } = parseEnvArgs(commandArr.slice(1));
|
|
47
|
+
const actualIndex = commandIndex + 1;
|
|
48
|
+
return {
|
|
49
|
+
transport: "stdio",
|
|
50
|
+
command: commandArr[actualIndex] ?? "",
|
|
51
|
+
args: commandArr.slice(actualIndex + 1),
|
|
52
|
+
env,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [command = "", ...args] = commandArr;
|
|
57
|
+
return { transport: "stdio", command, args, env: {} };
|
|
58
|
+
}
|
|
59
|
+
if (type === "remote") {
|
|
60
|
+
return {
|
|
61
|
+
transport: "http",
|
|
62
|
+
url: raw["url"] as string,
|
|
63
|
+
headers: (raw["headers"] as Record<string, string>) ?? {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const opencodeAdapter: AgentAdapter = {
|
|
70
|
+
id: "opencode",
|
|
71
|
+
name: "OpenCode",
|
|
72
|
+
configPath: (projectDir) => join(projectDir, "opencode.json"),
|
|
73
|
+
isGlobal: false,
|
|
74
|
+
|
|
75
|
+
toAgentFormat,
|
|
76
|
+
fromAgentFormat,
|
|
77
|
+
|
|
78
|
+
async read(projectDir) {
|
|
79
|
+
const filePath = join(projectDir, "opencode.json");
|
|
80
|
+
const data = await readJsonFile(filePath);
|
|
81
|
+
return (data["mcp"] as Record<string, unknown>) ?? {};
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async write(projectDir, serverName, config) {
|
|
85
|
+
const filePath = join(projectDir, "opencode.json");
|
|
86
|
+
const data = await readJsonFile(filePath);
|
|
87
|
+
const servers = (data["mcp"] as Record<string, unknown>) ?? {};
|
|
88
|
+
if (serverName in servers) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Conflict: "${serverName}" already exists in OpenCode config`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const updated = {
|
|
94
|
+
...data,
|
|
95
|
+
mcp: { ...servers, [serverName]: toAgentFormat(config) },
|
|
96
|
+
};
|
|
97
|
+
await writeJsonFile(filePath, updated);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async remove(projectDir, serverName) {
|
|
101
|
+
const filePath = join(projectDir, "opencode.json");
|
|
102
|
+
const data = await readJsonFile(filePath);
|
|
103
|
+
const servers = (data["mcp"] as Record<string, unknown>) ?? {};
|
|
104
|
+
const { [serverName]: _, ...rest } = servers;
|
|
105
|
+
await writeJsonFile(filePath, { ...data, mcp: rest });
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async has(projectDir, serverName) {
|
|
109
|
+
const filePath = join(projectDir, "opencode.json");
|
|
110
|
+
const data = await readJsonFile(filePath);
|
|
111
|
+
const servers = (data["mcp"] as Record<string, unknown>) ?? {};
|
|
112
|
+
return serverName in servers;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { detectAgents } from "../adapters/index.js";
|
|
3
|
+
import { readServerDefinition, serverExists } from "../utils/server-store.js";
|
|
4
|
+
import { resolveConfig } from "../utils/resolve-config.js";
|
|
5
|
+
import { isUserCancellation } from "../utils/prompt.js";
|
|
6
|
+
import type { AgentAdapter } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export async function addCommand(serverName: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
await addCommandInner(serverName);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isUserCancellation(error)) return;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function addCommandInner(serverName: string): Promise<void> {
|
|
18
|
+
const projectDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
if (!serverExists(serverName)) {
|
|
21
|
+
console.error(
|
|
22
|
+
`Error: Server "${serverName}" not found in central repository.`,
|
|
23
|
+
);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const definition = await readServerDefinition(serverName);
|
|
29
|
+
if (!definition) {
|
|
30
|
+
console.error(`Error: Failed to read server definition for "${serverName}".`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const detected = detectAgents(projectDir);
|
|
36
|
+
if (detected.length === 0) {
|
|
37
|
+
console.log(
|
|
38
|
+
"No agent config files detected in this project. Use \"mcpsmgr init\" first.",
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const selectedAgents = await checkbox<AgentAdapter>({
|
|
44
|
+
message: `Select agents to add "${serverName}" to:`,
|
|
45
|
+
choices: detected.map((adapter) => ({
|
|
46
|
+
name: `${adapter.name}${adapter.isGlobal ? " [global]" : ""}`,
|
|
47
|
+
value: adapter,
|
|
48
|
+
checked: !adapter.isGlobal,
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (selectedAgents.length === 0) {
|
|
53
|
+
console.log("No agents selected.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const agent of selectedAgents) {
|
|
58
|
+
try {
|
|
59
|
+
const config = resolveConfig(definition, agent);
|
|
60
|
+
await agent.write(projectDir, serverName, config);
|
|
61
|
+
console.log(` + ${serverName} -> ${agent.name}`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(
|
|
64
|
+
` ! ${serverName} -> ${agent.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { allAdapters, detectAgents } from "../adapters/index.js";
|
|
3
|
+
import { listServerDefinitions } from "../utils/server-store.js";
|
|
4
|
+
import { resolveConfig } from "../utils/resolve-config.js";
|
|
5
|
+
import { isUserCancellation } from "../utils/prompt.js";
|
|
6
|
+
import type { AgentAdapter } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export async function initCommand(): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
await initCommandInner();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isUserCancellation(error)) return;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function initCommandInner(): Promise<void> {
|
|
18
|
+
const projectDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
const servers = await listServerDefinitions();
|
|
21
|
+
if (servers.length === 0) {
|
|
22
|
+
console.log(
|
|
23
|
+
"Central repository is empty. Use \"mcpsmgr server add\" to add servers first.",
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const detected = detectAgents(projectDir);
|
|
29
|
+
const detectedIds = new Set(detected.map((a) => a.id));
|
|
30
|
+
|
|
31
|
+
const selectedAgents = await checkbox<AgentAdapter>({
|
|
32
|
+
message: "Select agents to configure:",
|
|
33
|
+
choices: allAdapters.map((adapter) => ({
|
|
34
|
+
name: `${adapter.name}${detectedIds.has(adapter.id) ? " (detected)" : ""}${adapter.isGlobal ? " [global]" : ""}`,
|
|
35
|
+
value: adapter,
|
|
36
|
+
checked: detectedIds.has(adapter.id) && !adapter.isGlobal,
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (selectedAgents.length === 0) {
|
|
41
|
+
console.log("No agents selected.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const agentServerMap = new Map<string, Set<string>>();
|
|
46
|
+
const detectedServers = new Set<string>();
|
|
47
|
+
for (const agent of selectedAgents) {
|
|
48
|
+
try {
|
|
49
|
+
const existing = await agent.read(projectDir);
|
|
50
|
+
const names = new Set(Object.keys(existing));
|
|
51
|
+
agentServerMap.set(agent.id, names);
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
detectedServers.add(name);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// silent fallback per design decision
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const selectedServers = await checkbox({
|
|
61
|
+
message: "Select servers to deploy:",
|
|
62
|
+
choices: servers.map((s) => ({
|
|
63
|
+
name: `${s.name}${detectedServers.has(s.name) ? " (detected)" : ""} [${s.default.transport}]`,
|
|
64
|
+
value: s,
|
|
65
|
+
checked: detectedServers.has(s.name),
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const selectedServerNames = new Set(selectedServers.map((s) => s.name));
|
|
70
|
+
|
|
71
|
+
const removals = new Map<string, AgentAdapter[]>();
|
|
72
|
+
for (const serverName of detectedServers) {
|
|
73
|
+
if (!selectedServerNames.has(serverName)) {
|
|
74
|
+
const agents = selectedAgents.filter((a) => {
|
|
75
|
+
const agentServers = agentServerMap.get(a.id);
|
|
76
|
+
return agentServers?.has(serverName);
|
|
77
|
+
});
|
|
78
|
+
if (agents.length > 0) {
|
|
79
|
+
removals.set(serverName, agents);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (selectedServers.length === 0 && removals.size === 0) {
|
|
85
|
+
console.log("No servers selected.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log("\nPlan:");
|
|
90
|
+
for (const agent of selectedAgents) {
|
|
91
|
+
console.log(` ${agent.name}:`);
|
|
92
|
+
for (const server of selectedServers) {
|
|
93
|
+
console.log(` + ${server.name}`);
|
|
94
|
+
}
|
|
95
|
+
for (const [serverName, agents] of removals) {
|
|
96
|
+
if (agents.some((a) => a.id === agent.id)) {
|
|
97
|
+
console.log(` - ${serverName}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const proceed = await confirm({ message: "Proceed?" });
|
|
103
|
+
if (!proceed) {
|
|
104
|
+
console.log("Cancelled.");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const agent of selectedAgents) {
|
|
109
|
+
for (const server of selectedServers) {
|
|
110
|
+
try {
|
|
111
|
+
const config = resolveConfig(server, agent);
|
|
112
|
+
await agent.write(projectDir, server.name, config);
|
|
113
|
+
console.log(` + ${server.name} -> ${agent.name}`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn(
|
|
116
|
+
` ! ${server.name} -> ${agent.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [serverName, agents] of removals) {
|
|
123
|
+
for (const agent of agents) {
|
|
124
|
+
try {
|
|
125
|
+
await agent.remove(projectDir, serverName);
|
|
126
|
+
console.log(` - ${serverName} <- ${agent.name}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(
|
|
129
|
+
` ! ${serverName} <- ${agent.name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log("\nDone.");
|
|
136
|
+
}
|