s-app-scoder-agent 0.8.20 → 0.9.2

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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Phase 5 — MCP config import: parse server configs from third-party formats
3
+ * (Claude Desktop, OpenCode, .mcp.json) and merge them into our format.
4
+ *
5
+ * Supports:
6
+ * - Claude Desktop: `~/.claude/config.json` → `mcpServers` (stdio + env)
7
+ * - OpenCode: `opencode.json` → `mcp` (stdio or http)
8
+ * - Standard: `.mcp.json` → array of `{name, command, url, …}`
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ /** Normalise a Claude Desktop mcpServers entry to our McpServerConfig. */
14
+ function fromClaudeDesktop(raw, _name) {
15
+ const cfg = {};
16
+ if (typeof raw.command === "string") {
17
+ cfg.command = raw.command;
18
+ if (Array.isArray(raw.args))
19
+ cfg.args = raw.args;
20
+ }
21
+ else if (typeof raw.url === "string") {
22
+ cfg.url = raw.url;
23
+ if (typeof raw.headers === "object" && raw.headers !== null) {
24
+ cfg.headers = raw.headers;
25
+ }
26
+ }
27
+ if (typeof raw.env === "object" && raw.env !== null) {
28
+ cfg.env = raw.env;
29
+ }
30
+ if (typeof raw.autoApprove === "boolean") {
31
+ // Claude Desktop autoApprove maps to our trust levels:
32
+ // autoApprove=true → "auto" (trust the server to run without prompt)
33
+ // autoApprove=false → "ask" (default, prompt before each tool call)
34
+ // We deliberately do NOT set trust here — let the global mode apply.
35
+ }
36
+ return cfg;
37
+ }
38
+ /** Normalise an OpenCode mcp config entry to our McpServerConfig. */
39
+ function fromOpenCode(raw, _name) {
40
+ const cfg = {};
41
+ if (typeof raw.command === "string") {
42
+ cfg.command = raw.command;
43
+ if (Array.isArray(raw.args))
44
+ cfg.args = raw.args;
45
+ }
46
+ else if (typeof raw.url === "string") {
47
+ cfg.url = raw.url;
48
+ if (typeof raw.headers === "object" && raw.headers !== null) {
49
+ cfg.headers = raw.headers;
50
+ }
51
+ }
52
+ if (typeof raw.env === "object" && raw.env !== null) {
53
+ cfg.env = raw.env;
54
+ }
55
+ if (raw.disabled === true)
56
+ cfg.enabled = false;
57
+ return cfg;
58
+ }
59
+ /** Parse Claude Desktop `mcpServers` config from a JSON string. */
60
+ function parseClaude(raw, sourcePath) {
61
+ const parsed = JSON.parse(raw);
62
+ const servers = [];
63
+ const entries = parsed?.mcpServers ?? parsed?.mcp?.servers ?? {};
64
+ for (const [key, value] of Object.entries(entries)) {
65
+ if (!value || typeof value !== "object")
66
+ continue;
67
+ const rec = value;
68
+ if (rec.enabled === false)
69
+ continue;
70
+ const config = fromClaudeDesktop(rec, key);
71
+ if (!config.command && !config.url)
72
+ continue;
73
+ servers.push({ source: sourcePath, name: key, originalKey: key, config });
74
+ }
75
+ return servers;
76
+ }
77
+ /** Parse OpenCode `mcp` config from a JSON string. */
78
+ function parseOpenCode(raw, sourcePath) {
79
+ const parsed = JSON.parse(raw);
80
+ const servers = [];
81
+ const entries = parsed?.mcp ?? parsed?.servers ?? {};
82
+ for (const [key, value] of Object.entries(entries)) {
83
+ if (!value || typeof value !== "object")
84
+ continue;
85
+ const rec = value;
86
+ // OpenCode: "disabled" flag; our config: "enabled: false". Both mean skip.
87
+ if (rec.disabled === true || rec.enabled === false)
88
+ continue;
89
+ const config = fromOpenCode(rec, key);
90
+ if (!config.command && !config.url)
91
+ continue;
92
+ servers.push({ source: sourcePath, name: key, originalKey: key, config });
93
+ }
94
+ return servers;
95
+ }
96
+ /** Parse standard `.mcp.json` array of server configs. */
97
+ function parseMcpJson(raw, sourcePath) {
98
+ const parsed = JSON.parse(raw);
99
+ const servers = [];
100
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
101
+ for (const entry of entries) {
102
+ if (!entry || typeof entry !== "object")
103
+ continue;
104
+ const rec = entry;
105
+ const name = typeof rec.name === "string" ? rec.name : "unknown";
106
+ if (rec.enabled === false)
107
+ continue;
108
+ const cfg = {};
109
+ if (typeof rec.command === "string") {
110
+ cfg.command = rec.command;
111
+ if (Array.isArray(rec.args))
112
+ cfg.args = rec.args;
113
+ }
114
+ else if (typeof rec.url === "string") {
115
+ cfg.url = rec.url;
116
+ if (typeof rec.headers === "object" && rec.headers !== null) {
117
+ cfg.headers = rec.headers;
118
+ }
119
+ }
120
+ if (typeof rec.timeoutMs === "number")
121
+ cfg.timeoutMs = rec.timeoutMs;
122
+ if (typeof rec.env === "object" && rec.env !== null) {
123
+ cfg.env = rec.env;
124
+ }
125
+ if (typeof rec.trust === "string")
126
+ cfg.trust = rec.trust;
127
+ if (typeof rec.enabled === "boolean")
128
+ cfg.enabled = rec.enabled;
129
+ if (!cfg.command && !cfg.url)
130
+ continue;
131
+ servers.push({ source: sourcePath, name, originalKey: name, config: cfg });
132
+ }
133
+ return servers;
134
+ }
135
+ /** Detect format and parse a source file; throws on unreadable/invalid JSON. */
136
+ export function parseConfigFile(raw, sourcePath) {
137
+ // Heuristic: if it has `mcpServers` top-level → Claude Desktop.
138
+ // If it has `mcp` top-level (object or array) → OpenCode or standard MCP.
139
+ let parsed;
140
+ try {
141
+ parsed = JSON.parse(raw);
142
+ }
143
+ catch {
144
+ throw new Error(`Invalid JSON in ${sourcePath}`);
145
+ }
146
+ if ("mcpServers" in parsed)
147
+ return parseClaude(raw, sourcePath);
148
+ if ("mcp" in parsed)
149
+ return parseOpenCode(raw, sourcePath);
150
+ // Fallback: treat as standard MCP JSON array.
151
+ return parseMcpJson(raw, sourcePath);
152
+ }
153
+ /**
154
+ * Load + parse one config file by path.
155
+ * Returns `null` when the file does not exist; throws on unreadable/invalid.
156
+ */
157
+ export function loadConfigFile(path) {
158
+ let raw;
159
+ try {
160
+ raw = readFileSync(path, "utf8");
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ return parseConfigFile(raw, path);
166
+ }
167
+ export function discoverConfigs() {
168
+ const found = [];
169
+ const checks = [
170
+ [join(homedir(), ".claude", "config.json"), "claude"],
171
+ [join(homedir(), ".config", "claude", "config.json"), "claude"],
172
+ [join(process.cwd(), "opencode.json"), "opencode"],
173
+ [join(process.cwd(), ".mcp.json"), "mcp-json"],
174
+ ];
175
+ for (const [path, format] of checks) {
176
+ try {
177
+ const raw = readFileSync(path, "utf8");
178
+ const servers = parseConfigFile(raw, path);
179
+ if (servers.length > 0)
180
+ found.push({ source: path, format, serverCount: servers.length });
181
+ }
182
+ catch { /* not present */ }
183
+ }
184
+ return found;
185
+ }
186
+ /** Merge imported servers into an existing config map. */
187
+ export function mergeConfigs(existing, imported, opts = {}) {
188
+ const added = [];
189
+ const skipped = [];
190
+ const updated = [];
191
+ for (const srv of imported) {
192
+ const existingCfg = existing[srv.name];
193
+ if (existingCfg && !opts.force) {
194
+ skipped.push(srv);
195
+ continue;
196
+ }
197
+ if (existingCfg && opts.force)
198
+ updated.push(srv.name);
199
+ if (!opts.dryRun)
200
+ existing[srv.name] = srv.config;
201
+ added.push(srv);
202
+ }
203
+ return { added, skipped, updated };
204
+ }