superacli 1.1.2 → 1.1.4
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 +22 -0
- package/README.md +10 -1
- package/__tests__/blogwatcher-plugin.test.js +157 -0
- package/__tests__/clix-plugin.test.js +143 -0
- package/__tests__/config.test.js +46 -1
- package/__tests__/himalaya-plugin.test.js +121 -0
- package/__tests__/mcp-adapter.test.js +79 -0
- package/__tests__/mcp-local.test.js +43 -1
- package/__tests__/mongosh-plugin.test.js +106 -0
- package/__tests__/mysql-plugin.test.js +94 -0
- package/__tests__/plugin-blogwatcher.test.js +55 -0
- package/__tests__/plugin-clix.test.js +51 -0
- package/__tests__/plugin-xurl.test.js +51 -0
- package/__tests__/server-config-service.test.js +8 -1
- package/__tests__/skills.test.js +26 -0
- package/__tests__/wacli-plugin.test.js +132 -0
- package/__tests__/xurl-plugin.test.js +176 -0
- package/cli/adapter-schema.js +7 -0
- package/cli/adapters/mcp.js +82 -20
- package/cli/config.js +65 -8
- package/cli/mcp-local.js +50 -4
- package/cli/plugin-install-guidance.js +100 -0
- package/cli/skills.js +55 -0
- package/cli/supercli.js +1 -1
- package/docs/features/adapters.md +6 -2
- package/docs/initial/mcp-local-mode.md +3 -0
- package/docs/skills-catalog.md +50 -0
- package/docs/supported-harnesses.md +20 -0
- package/package.json +2 -1
- package/plugins/blogwatcher/README.md +52 -0
- package/plugins/blogwatcher/plugin.json +195 -0
- package/plugins/blogwatcher/scripts/post-install.js +66 -0
- package/plugins/blogwatcher/scripts/post-uninstall.js +25 -0
- package/plugins/clix/README.md +44 -0
- package/plugins/clix/plugin.json +126 -0
- package/plugins/clix/scripts/post-install.js +66 -0
- package/plugins/clix/scripts/post-uninstall.js +25 -0
- package/plugins/himalaya/README.md +48 -0
- package/plugins/himalaya/plugin.json +157 -0
- package/plugins/mongosh/README.md +56 -0
- package/plugins/mongosh/plugin.json +88 -0
- package/plugins/mysql/README.md +48 -0
- package/plugins/mysql/plugin.json +64 -0
- package/plugins/plugins.json +63 -0
- package/plugins/wacli/README.md +52 -0
- package/plugins/wacli/plugin.json +260 -0
- package/plugins/xurl/README.md +52 -0
- package/plugins/xurl/plugin.json +239 -0
- package/plugins/xurl/scripts/post-install.js +66 -0
- package/plugins/xurl/scripts/post-uninstall.js +25 -0
- package/server/routes/mcp.js +30 -4
- package/server/services/configService.js +9 -1
- package/tests/test-blogwatcher-smoke.sh +48 -0
- package/tests/test-clix-smoke.sh +44 -0
- package/tests/test-himalaya-smoke.sh +47 -0
- package/tests/test-mcp-browser-use-smoke.sh +141 -0
- package/tests/test-mongosh-smoke.sh +40 -0
- package/tests/test-mysql-smoke.sh +37 -0
- package/tests/test-plugins-registry.js +35 -0
- package/tests/test-wacli-smoke.sh +46 -0
- package/tests/test-xurl-smoke.sh +46 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const os = require("os")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const { execSync } = require("child_process")
|
|
5
|
+
|
|
6
|
+
const CLI = path.join(__dirname, "..", "cli", "supercli.js")
|
|
7
|
+
|
|
8
|
+
function runNoServer(args, options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
const env = { ...process.env }
|
|
11
|
+
delete env.SUPERCLI_SERVER
|
|
12
|
+
const out = execSync(`node ${CLI} ${args}`, {
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
timeout: 15000,
|
|
15
|
+
env: { ...env, ...(options.env || {}) }
|
|
16
|
+
})
|
|
17
|
+
return { ok: true, output: out.trim(), code: 0 }
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
output: (err.stdout || "").trim(),
|
|
22
|
+
stderr: (err.stderr || "").trim(),
|
|
23
|
+
code: err.status
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeFakeCurlBinary(dir) {
|
|
29
|
+
const bin = path.join(dir, "curl")
|
|
30
|
+
fs.writeFileSync(bin, [
|
|
31
|
+
"#!/usr/bin/env node",
|
|
32
|
+
"const args = process.argv.slice(2);",
|
|
33
|
+
"const url = args[args.length - 1] || '';",
|
|
34
|
+
"if (args.includes('--version')) { console.log('curl 8.0.0-test'); process.exit(0); }",
|
|
35
|
+
"if (url.endsWith('/SKILL.md')) { console.log('---\\nname: xurl\\ndescription: Test skill\\n---\\n# xurl Skill'); process.exit(0); }",
|
|
36
|
+
"if (url.endsWith('/README.md')) { console.log('# xurl\\n\\nTest readme.'); process.exit(0); }",
|
|
37
|
+
"console.error(`unsupported url: ${url}`);",
|
|
38
|
+
"process.exit(22);"
|
|
39
|
+
].join("\n"), "utf-8")
|
|
40
|
+
fs.chmodSync(bin, 0o755)
|
|
41
|
+
return bin
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeFakeXurlBinary(dir) {
|
|
45
|
+
const bin = path.join(dir, "xurl")
|
|
46
|
+
fs.writeFileSync(bin, [
|
|
47
|
+
"#!/usr/bin/env node",
|
|
48
|
+
"const args = process.argv.slice(2);",
|
|
49
|
+
"const emitJson = value => {",
|
|
50
|
+
" const payload = JSON.stringify(value);",
|
|
51
|
+
" if (process.env.NO_COLOR === '1') console.log(payload);",
|
|
52
|
+
" else console.log(`\\u001b[32m${payload}\\u001b[0m`);",
|
|
53
|
+
" process.exit(0);",
|
|
54
|
+
"};",
|
|
55
|
+
"if (args[0] === 'version') { console.log('xurl 1.0.3-test'); process.exit(0); }",
|
|
56
|
+
"if (args[0] === 'auth' && args[1] === 'status') { console.log('▸ my-app [client_id: abc123…]'); process.exit(0); }",
|
|
57
|
+
"if (args[0] === 'auth' && args[1] === 'apps' && args[2] === 'list') { console.log('▸ my-app (client_id: abc123…)'); process.exit(0); }",
|
|
58
|
+
"if (args[0] === 'whoami') emitJson({ data: { id: '42', username: 'tester' } });",
|
|
59
|
+
"if (args[0] === 'user') emitJson({ data: { id: '84', username: args[1].replace(/^@/, '') } });",
|
|
60
|
+
"if (args[0] === 'read') emitJson({ data: { id: '123', text: 'hello world', ref: args[1] } });",
|
|
61
|
+
"if (args[0] === 'search') emitJson({ data: [{ id: '1', text: args[1] }] });",
|
|
62
|
+
"if (args[0] === 'timeline') emitJson({ data: [{ id: '2', text: 'timeline item' }] });",
|
|
63
|
+
"if (args[0] === 'mentions') emitJson({ data: [{ id: '3', text: 'mention item' }] });",
|
|
64
|
+
"if (args[0] === 'followers') emitJson({ data: [{ username: args.includes('--of') ? args[args.indexOf('--of') + 1] : 'alice' }] });",
|
|
65
|
+
"if (args[0] === 'following') emitJson({ data: [{ username: args.includes('--of') ? args[args.indexOf('--of') + 1] : 'bob' }] });",
|
|
66
|
+
"emitJson({ ok: true, args });"
|
|
67
|
+
].join("\n"), "utf-8")
|
|
68
|
+
fs.chmodSync(bin, 0o755)
|
|
69
|
+
return bin
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("xurl hybrid plugin", () => {
|
|
73
|
+
const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-xurl-"))
|
|
74
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcli-home-xurl-"))
|
|
75
|
+
writeFakeCurlBinary(fakeDir)
|
|
76
|
+
writeFakeXurlBinary(fakeDir)
|
|
77
|
+
const env = { ...process.env, PATH: `${fakeDir}:${process.env.PATH || ""}`, SUPERCLI_HOME: tempHome }
|
|
78
|
+
let removed = false
|
|
79
|
+
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
const install = runNoServer("plugins install ./plugins/xurl --on-conflict replace --json", { env })
|
|
82
|
+
expect(install.ok).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
afterAll(() => {
|
|
86
|
+
if (!removed) runNoServer("plugins remove xurl --json", { env })
|
|
87
|
+
fs.rmSync(fakeDir, { recursive: true, force: true })
|
|
88
|
+
fs.rmSync(tempHome, { recursive: true, force: true })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("indexes xurl skills provider", () => {
|
|
92
|
+
const provider = runNoServer("skills providers show --name xurl --json", { env })
|
|
93
|
+
expect(provider.ok).toBe(true)
|
|
94
|
+
expect(JSON.parse(provider.output).provider.name).toBe("xurl")
|
|
95
|
+
|
|
96
|
+
const list = runNoServer("skills list --catalog --provider xurl --json", { env })
|
|
97
|
+
expect(list.ok).toBe(true)
|
|
98
|
+
const listData = JSON.parse(list.output)
|
|
99
|
+
expect(listData.skills.some(skill => skill.id === "xurl:root.skill")).toBe(true)
|
|
100
|
+
expect(listData.skills.some(skill => skill.id === "xurl:root.readme")).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("fetches indexed remote skill markdown", () => {
|
|
104
|
+
const skill = runNoServer("skills get xurl:root.skill", { env })
|
|
105
|
+
expect(skill.ok).toBe(true)
|
|
106
|
+
expect(skill.output).toContain("xurl Skill")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("routes wrapped raw and parsed commands", () => {
|
|
110
|
+
const version = runNoServer("xurl cli version --json", { env })
|
|
111
|
+
expect(version.ok).toBe(true)
|
|
112
|
+
expect(JSON.parse(version.output).data.raw).toBe("xurl 1.0.3-test")
|
|
113
|
+
|
|
114
|
+
const status = runNoServer("xurl auth status --json", { env })
|
|
115
|
+
expect(status.ok).toBe(true)
|
|
116
|
+
expect(JSON.parse(status.output).data.raw).toContain("my-app")
|
|
117
|
+
|
|
118
|
+
const apps = runNoServer("xurl apps list --json", { env })
|
|
119
|
+
expect(apps.ok).toBe(true)
|
|
120
|
+
expect(JSON.parse(apps.output).data.raw).toContain("my-app")
|
|
121
|
+
|
|
122
|
+
const whoami = runNoServer("xurl account whoami --json", { env })
|
|
123
|
+
expect(whoami.ok).toBe(true)
|
|
124
|
+
expect(JSON.parse(whoami.output).data.data.username).toBe("tester")
|
|
125
|
+
|
|
126
|
+
const user = runNoServer("xurl users show --target @XDevelopers --json", { env })
|
|
127
|
+
expect(user.ok).toBe(true)
|
|
128
|
+
expect(JSON.parse(user.output).data.data.username).toBe("XDevelopers")
|
|
129
|
+
|
|
130
|
+
const read = runNoServer("xurl posts show --target 1234567890 --json", { env })
|
|
131
|
+
expect(read.ok).toBe(true)
|
|
132
|
+
expect(JSON.parse(read.output).data.data.ref).toBe("1234567890")
|
|
133
|
+
|
|
134
|
+
const search = runNoServer("xurl posts search --query \"from:XDevelopers\" --max-results 10 --json", { env })
|
|
135
|
+
expect(search.ok).toBe(true)
|
|
136
|
+
expect(JSON.parse(search.output).data.data[0].text).toBe("from:XDevelopers")
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("routes timeline mentions and social wrappers", () => {
|
|
140
|
+
const timeline = runNoServer("xurl timeline list --max-results 10 --json", { env })
|
|
141
|
+
expect(timeline.ok).toBe(true)
|
|
142
|
+
expect(JSON.parse(timeline.output).data.data[0].text).toBe("timeline item")
|
|
143
|
+
|
|
144
|
+
const mentions = runNoServer("xurl mentions list --max-results 10 --json", { env })
|
|
145
|
+
expect(mentions.ok).toBe(true)
|
|
146
|
+
expect(JSON.parse(mentions.output).data.data[0].text).toBe("mention item")
|
|
147
|
+
|
|
148
|
+
const followers = runNoServer("xurl social followers --of XDevelopers --max-results 20 --json", { env })
|
|
149
|
+
expect(followers.ok).toBe(true)
|
|
150
|
+
expect(JSON.parse(followers.output).data.data[0].username).toBe("XDevelopers")
|
|
151
|
+
|
|
152
|
+
const following = runNoServer("xurl social following --of XDevelopers --max-results 20 --json", { env })
|
|
153
|
+
expect(following.ok).toBe(true)
|
|
154
|
+
expect(JSON.parse(following.output).data.data[0].username).toBe("XDevelopers")
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test("does not expose passthrough and reports dependencies as healthy", () => {
|
|
158
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "plugins", "xurl", "plugin.json"), "utf-8"))
|
|
159
|
+
expect(manifest.commands.some(command => command.resource === "_" && command.action === "_")).toBe(false)
|
|
160
|
+
|
|
161
|
+
const doctor = runNoServer("plugins doctor xurl --json", { env })
|
|
162
|
+
expect(doctor.ok).toBe(true)
|
|
163
|
+
const doctorData = JSON.parse(doctor.output)
|
|
164
|
+
expect(doctorData.checks.some(c => c.type === "binary" && c.binary === "curl" && c.ok === true)).toBe(true)
|
|
165
|
+
expect(doctorData.checks.some(c => c.type === "binary" && c.binary === "xurl" && c.ok === true)).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("removal cleans up the skills provider", () => {
|
|
169
|
+
const remove = runNoServer("plugins remove xurl --json", { env })
|
|
170
|
+
expect(remove.ok).toBe(true)
|
|
171
|
+
removed = true
|
|
172
|
+
|
|
173
|
+
const provider = runNoServer("skills providers show --name xurl --json", { env })
|
|
174
|
+
expect(provider.ok).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
})
|
package/cli/adapter-schema.js
CHANGED
|
@@ -69,6 +69,13 @@ function validateAdapterConfig(cmd) {
|
|
|
69
69
|
if (!config.tool || typeof config.tool !== "string") throw asInvalid("MCP adapter requires adapterConfig.tool")
|
|
70
70
|
const sources = [config.server, config.url, config.command].filter(Boolean)
|
|
71
71
|
if (sources.length === 0) throw asInvalid("MCP adapter requires one source: adapterConfig.server, adapterConfig.url, or adapterConfig.command")
|
|
72
|
+
if (config.server !== undefined && typeof config.server !== "string") throw asInvalid("adapterConfig.server must be string")
|
|
73
|
+
if (config.url !== undefined && typeof config.url !== "string") throw asInvalid("adapterConfig.url must be string")
|
|
74
|
+
if (config.command !== undefined && typeof config.command !== "string") throw asInvalid("adapterConfig.command must be string")
|
|
75
|
+
if (config.args !== undefined && !Array.isArray(config.args)) throw asInvalid("adapterConfig.args must be an array")
|
|
76
|
+
if (config.commandArgs !== undefined && !Array.isArray(config.commandArgs)) throw asInvalid("adapterConfig.commandArgs must be an array")
|
|
77
|
+
if (config.headers !== undefined && (typeof config.headers !== "object" || Array.isArray(config.headers))) throw asInvalid("adapterConfig.headers must be object")
|
|
78
|
+
if (config.env !== undefined && (typeof config.env !== "object" || Array.isArray(config.env))) throw asInvalid("adapterConfig.env must be object")
|
|
72
79
|
return
|
|
73
80
|
}
|
|
74
81
|
|
package/cli/adapters/mcp.js
CHANGED
|
@@ -3,11 +3,63 @@
|
|
|
3
3
|
|
|
4
4
|
const { spawn } = require("child_process");
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function asObject(value) {
|
|
7
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
8
|
+
? value
|
|
9
|
+
: {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function asStringMap(value) {
|
|
13
|
+
const obj = asObject(value);
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
16
|
+
if (typeof k === "string" && typeof v === "string") out[k] = v;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asStringArray(value) {
|
|
22
|
+
if (!Array.isArray(value)) return [];
|
|
23
|
+
return value.filter((v) => typeof v === "string");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mergeMcpConfig(cmdConfig, serverEntry) {
|
|
27
|
+
const mergedHeaders = {
|
|
28
|
+
...asStringMap(serverEntry && serverEntry.headers),
|
|
29
|
+
...asStringMap(cmdConfig.headers),
|
|
30
|
+
};
|
|
31
|
+
const mergedEnv = {
|
|
32
|
+
...asStringMap(serverEntry && serverEntry.env),
|
|
33
|
+
...asStringMap(cmdConfig.env),
|
|
34
|
+
};
|
|
35
|
+
const mergedArgs = [
|
|
36
|
+
...asStringArray(serverEntry && serverEntry.args),
|
|
37
|
+
...asStringArray(serverEntry && serverEntry.commandArgs),
|
|
38
|
+
...asStringArray(cmdConfig.args),
|
|
39
|
+
...asStringArray(cmdConfig.commandArgs),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
tool: cmdConfig.tool,
|
|
44
|
+
server: cmdConfig.server,
|
|
45
|
+
url: cmdConfig.url || (serverEntry && serverEntry.url),
|
|
46
|
+
command: cmdConfig.command || (serverEntry && serverEntry.command),
|
|
47
|
+
timeout_ms:
|
|
48
|
+
cmdConfig.timeout_ms !== undefined
|
|
49
|
+
? cmdConfig.timeout_ms
|
|
50
|
+
: serverEntry && serverEntry.timeout_ms,
|
|
51
|
+
headers: mergedHeaders,
|
|
52
|
+
env: mergedEnv,
|
|
53
|
+
args: mergedArgs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function callStdioTool(command, args, payload, timeoutMs, env) {
|
|
7
58
|
return new Promise((resolve, reject) => {
|
|
8
59
|
const child = spawn(command, args || [], {
|
|
9
60
|
stdio: ["pipe", "pipe", "pipe"],
|
|
10
61
|
shell: !Array.isArray(args) || args.length === 0,
|
|
62
|
+
env: env && typeof env === "object" ? { ...process.env, ...env } : process.env,
|
|
11
63
|
});
|
|
12
64
|
|
|
13
65
|
let stdout = "";
|
|
@@ -95,17 +147,15 @@ async function callStdioTool(command, args, payload, timeoutMs) {
|
|
|
95
147
|
});
|
|
96
148
|
}
|
|
97
149
|
|
|
98
|
-
async function
|
|
99
|
-
if (config.
|
|
100
|
-
|
|
101
|
-
if (context.config && Array.isArray(context.config.mcp_servers)) {
|
|
150
|
+
async function resolveServerEntry(config, context) {
|
|
151
|
+
if (config.server && context.config && Array.isArray(context.config.mcp_servers)) {
|
|
102
152
|
const local = context.config.mcp_servers.find(
|
|
103
153
|
(s) => s && s.name === config.server,
|
|
104
154
|
);
|
|
105
|
-
if (local) return local
|
|
155
|
+
if (local) return local;
|
|
106
156
|
}
|
|
107
157
|
|
|
108
|
-
if (context.server) {
|
|
158
|
+
if (config.server && context.server) {
|
|
109
159
|
const r = await fetch(`${context.server}/api/mcp?format=json`);
|
|
110
160
|
if (!r.ok) {
|
|
111
161
|
throw Object.assign(
|
|
@@ -118,10 +168,12 @@ async function resolveHttpServerUrl(config, context) {
|
|
|
118
168
|
);
|
|
119
169
|
}
|
|
120
170
|
const servers = await r.json();
|
|
121
|
-
const srv = servers.find((s) => s.name === config.server);
|
|
122
|
-
if (srv) return srv
|
|
171
|
+
const srv = servers.find((s) => s && s.name === config.server);
|
|
172
|
+
if (srv) return srv;
|
|
123
173
|
}
|
|
124
174
|
|
|
175
|
+
if (config.url || config.command) return {};
|
|
176
|
+
|
|
125
177
|
throw Object.assign(
|
|
126
178
|
new Error(
|
|
127
179
|
`MCP server '${config.server}' not found in local config. Add one with: supercli mcp add ${config.server} --url <mcp_url> or run supercli sync`,
|
|
@@ -135,17 +187,23 @@ async function resolveHttpServerUrl(config, context) {
|
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
async function execute(cmd, flags, context) {
|
|
138
|
-
const
|
|
139
|
-
const toolName =
|
|
140
|
-
const
|
|
141
|
-
const hasStdioSource = !!config.command;
|
|
190
|
+
const cmdConfig = cmd.adapterConfig || {};
|
|
191
|
+
const toolName = cmdConfig.tool;
|
|
192
|
+
const hasDeclaredSource = !!(cmdConfig.server || cmdConfig.url || cmdConfig.command);
|
|
142
193
|
|
|
143
|
-
if (!toolName ||
|
|
194
|
+
if (!toolName || !hasDeclaredSource) {
|
|
144
195
|
throw new Error(
|
|
145
196
|
"MCP adapter requires 'tool' and one of: 'server', 'url', or 'command' in adapterConfig",
|
|
146
197
|
);
|
|
147
198
|
}
|
|
148
199
|
|
|
200
|
+
const serverEntry = cmdConfig.server
|
|
201
|
+
? await resolveServerEntry(cmdConfig, context)
|
|
202
|
+
: {};
|
|
203
|
+
const config = mergeMcpConfig(cmdConfig, serverEntry);
|
|
204
|
+
const hasStdioSource = !!config.command;
|
|
205
|
+
const hasHttpSource = !!config.url;
|
|
206
|
+
|
|
149
207
|
const input = {};
|
|
150
208
|
for (const [k, v] of Object.entries(flags)) {
|
|
151
209
|
if (!["human", "json", "compact"].includes(k)) {
|
|
@@ -154,9 +212,7 @@ async function execute(cmd, flags, context) {
|
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
if (hasStdioSource) {
|
|
157
|
-
const commandArgs = Array.isArray(config.
|
|
158
|
-
? config.commandArgs
|
|
159
|
-
: [];
|
|
215
|
+
const commandArgs = Array.isArray(config.args) ? config.args : [];
|
|
160
216
|
const timeoutMs =
|
|
161
217
|
Number(config.timeout_ms) > 0 ? Number(config.timeout_ms) : 10000;
|
|
162
218
|
return callStdioTool(
|
|
@@ -164,14 +220,20 @@ async function execute(cmd, flags, context) {
|
|
|
164
220
|
commandArgs,
|
|
165
221
|
{ tool: toolName, input },
|
|
166
222
|
timeoutMs,
|
|
223
|
+
config.env,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!hasHttpSource) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"MCP adapter could not resolve a source. Define adapterConfig.url/command or configure the named server with url/command.",
|
|
167
230
|
);
|
|
168
231
|
}
|
|
169
232
|
|
|
170
|
-
const
|
|
171
|
-
const toolUrl = resolvedUrl.replace(/\/+$/, "");
|
|
233
|
+
const toolUrl = config.url.replace(/\/+$/, "");
|
|
172
234
|
const tr = await fetch(`${toolUrl}/tool`, {
|
|
173
235
|
method: "POST",
|
|
174
|
-
headers: { "Content-Type": "application/json" },
|
|
236
|
+
headers: { "Content-Type": "application/json", ...(config.headers || {}) },
|
|
175
237
|
body: JSON.stringify({ tool: toolName, input }),
|
|
176
238
|
});
|
|
177
239
|
|
package/cli/config.js
CHANGED
|
@@ -16,7 +16,8 @@ function readCache() {
|
|
|
16
16
|
try {
|
|
17
17
|
if (fs.existsSync(CACHE_FILE)) {
|
|
18
18
|
const raw = fs.readFileSync(CACHE_FILE, "utf-8")
|
|
19
|
-
|
|
19
|
+
const parsed = JSON.parse(raw)
|
|
20
|
+
return normalizeConfig(parsed)
|
|
20
21
|
}
|
|
21
22
|
} catch (e) {
|
|
22
23
|
// Corrupted cache, ignore
|
|
@@ -24,10 +25,60 @@ function readCache() {
|
|
|
24
25
|
return null
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function normalizeMcpServerEntry(name, entry) {
|
|
29
|
+
if (!name || typeof name !== "string") return null
|
|
30
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null
|
|
31
|
+
const out = { name }
|
|
32
|
+
if (typeof entry.url === "string") out.url = entry.url
|
|
33
|
+
if (typeof entry.command === "string") out.command = entry.command
|
|
34
|
+
if (Array.isArray(entry.args)) out.args = entry.args.filter(v => typeof v === "string")
|
|
35
|
+
if (Array.isArray(entry.commandArgs)) out.commandArgs = entry.commandArgs.filter(v => typeof v === "string")
|
|
36
|
+
if (entry.headers && typeof entry.headers === "object" && !Array.isArray(entry.headers)) {
|
|
37
|
+
out.headers = Object.fromEntries(Object.entries(entry.headers).filter(([k, v]) => typeof k === "string" && typeof v === "string"))
|
|
38
|
+
}
|
|
39
|
+
if (entry.env && typeof entry.env === "object" && !Array.isArray(entry.env)) {
|
|
40
|
+
out.env = Object.fromEntries(Object.entries(entry.env).filter(([k, v]) => typeof k === "string" && typeof v === "string"))
|
|
41
|
+
}
|
|
42
|
+
if (typeof entry.timeout_ms === "number" && entry.timeout_ms > 0) out.timeout_ms = entry.timeout_ms
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeMcpServers(config) {
|
|
47
|
+
const out = []
|
|
48
|
+
const byName = new Map()
|
|
49
|
+
|
|
50
|
+
if (config && config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)) {
|
|
51
|
+
for (const [name, value] of Object.entries(config.mcpServers)) {
|
|
52
|
+
const normalized = normalizeMcpServerEntry(name, value)
|
|
53
|
+
if (normalized) byName.set(name, normalized)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(config && config.mcp_servers)) {
|
|
58
|
+
for (const entry of config.mcp_servers) {
|
|
59
|
+
if (!entry || typeof entry !== "object") continue
|
|
60
|
+
const normalized = normalizeMcpServerEntry(entry.name, entry)
|
|
61
|
+
if (normalized) byName.set(normalized.name, normalized)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const value of byName.values()) out.push(value)
|
|
66
|
+
out.sort((a, b) => a.name.localeCompare(b.name))
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeConfig(config) {
|
|
71
|
+
const base = config && typeof config === "object" ? { ...config } : emptyConfig()
|
|
72
|
+
base.mcp_servers = normalizeMcpServers(base)
|
|
73
|
+
if (!Array.isArray(base.specs)) base.specs = []
|
|
74
|
+
if (!Array.isArray(base.commands)) base.commands = []
|
|
75
|
+
return base
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
function writeCache(config) {
|
|
28
79
|
ensureCacheDir()
|
|
29
80
|
const data = {
|
|
30
|
-
...config,
|
|
81
|
+
...normalizeConfig(config),
|
|
31
82
|
fetchedAt: Date.now()
|
|
32
83
|
}
|
|
33
84
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2))
|
|
@@ -85,19 +136,25 @@ async function syncConfig(server) {
|
|
|
85
136
|
}
|
|
86
137
|
|
|
87
138
|
if (!Array.isArray(config.commands)) config.commands = []
|
|
88
|
-
return writeCache(config)
|
|
139
|
+
return writeCache(normalizeConfig(config))
|
|
89
140
|
}
|
|
90
141
|
|
|
91
|
-
async function setMcpServer(name,
|
|
142
|
+
async function setMcpServer(name, value) {
|
|
92
143
|
const cfg = readCache() || emptyConfig()
|
|
93
144
|
const servers = Array.isArray(cfg.mcp_servers) ? cfg.mcp_servers.slice() : []
|
|
94
145
|
const idx = servers.findIndex(s => s && s.name === name)
|
|
95
|
-
const
|
|
146
|
+
const incoming = typeof value === "string" ? { url: value } : (value && typeof value === "object" ? value : {})
|
|
147
|
+
const next = normalizeMcpServerEntry(name, { name, ...incoming })
|
|
148
|
+
if (!next) {
|
|
149
|
+
throw Object.assign(new Error("Invalid MCP server definition"), {
|
|
150
|
+
code: 85,
|
|
151
|
+
type: "invalid_argument",
|
|
152
|
+
recoverable: false
|
|
153
|
+
})
|
|
154
|
+
}
|
|
96
155
|
if (idx >= 0) servers[idx] = next
|
|
97
156
|
else servers.push(next)
|
|
98
|
-
cfg.mcp_servers = servers
|
|
99
|
-
.filter(s => s && typeof s.name === "string")
|
|
100
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
157
|
+
cfg.mcp_servers = normalizeMcpServers({ mcp_servers: servers })
|
|
101
158
|
return writeCache(cfg)
|
|
102
159
|
}
|
|
103
160
|
|
package/cli/mcp-local.js
CHANGED
|
@@ -10,8 +10,25 @@ async function handleMcpRegistryCommand(options) {
|
|
|
10
10
|
removeMcpServer,
|
|
11
11
|
listMcpServers,
|
|
12
12
|
} = options;
|
|
13
|
+
const cliFlags = flags || {};
|
|
13
14
|
|
|
14
15
|
const subcommand = positional[1];
|
|
16
|
+
|
|
17
|
+
function parseJsonFlag(name, raw) {
|
|
18
|
+
if (raw === undefined) return undefined;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
outputError({
|
|
23
|
+
code: 85,
|
|
24
|
+
type: "invalid_argument",
|
|
25
|
+
message: `Invalid JSON for --${name}`,
|
|
26
|
+
recoverable: false,
|
|
27
|
+
});
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
if (subcommand === "list") {
|
|
16
33
|
const servers = await listMcpServers();
|
|
17
34
|
if (humanMode) {
|
|
@@ -29,17 +46,46 @@ async function handleMcpRegistryCommand(options) {
|
|
|
29
46
|
|
|
30
47
|
if (subcommand === "add") {
|
|
31
48
|
const name = positional[2];
|
|
32
|
-
const url =
|
|
33
|
-
|
|
49
|
+
const url = cliFlags.url;
|
|
50
|
+
const command = cliFlags.command;
|
|
51
|
+
|
|
52
|
+
if (!name || (!url && !command)) {
|
|
34
53
|
outputError({
|
|
35
54
|
code: 85,
|
|
36
55
|
type: "invalid_argument",
|
|
37
|
-
message:
|
|
56
|
+
message:
|
|
57
|
+
"Usage: supercli mcp add <name> (--url <mcp_url> | --command <binary>) [--args-json '[]'] [--headers-json '{}'] [--env-json '{}'] [--timeout-ms <ms>]",
|
|
38
58
|
recoverable: false,
|
|
39
59
|
});
|
|
40
60
|
return true;
|
|
41
61
|
}
|
|
42
|
-
|
|
62
|
+
|
|
63
|
+
const args = parseJsonFlag("args-json", cliFlags["args-json"]);
|
|
64
|
+
if (args === null) return true;
|
|
65
|
+
const headers = parseJsonFlag("headers-json", cliFlags["headers-json"]);
|
|
66
|
+
if (headers === null) return true;
|
|
67
|
+
const env = parseJsonFlag("env-json", cliFlags["env-json"]);
|
|
68
|
+
if (env === null) return true;
|
|
69
|
+
const timeoutMs =
|
|
70
|
+
cliFlags["timeout-ms"] !== undefined ? Number(cliFlags["timeout-ms"]) : undefined;
|
|
71
|
+
if (cliFlags["timeout-ms"] !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
72
|
+
outputError({
|
|
73
|
+
code: 85,
|
|
74
|
+
type: "invalid_argument",
|
|
75
|
+
message: "--timeout-ms must be a positive number",
|
|
76
|
+
recoverable: false,
|
|
77
|
+
});
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await setMcpServer(name, {
|
|
82
|
+
url,
|
|
83
|
+
command,
|
|
84
|
+
args,
|
|
85
|
+
headers,
|
|
86
|
+
env,
|
|
87
|
+
timeout_ms: timeoutMs,
|
|
88
|
+
});
|
|
43
89
|
output({ ok: true, message: `MCP server '${name}' saved locally` });
|
|
44
90
|
return true;
|
|
45
91
|
}
|
|
@@ -256,6 +256,106 @@ const PLUGIN_INSTALL_GUIDANCE = {
|
|
|
256
256
|
],
|
|
257
257
|
note: "Install cargo-nextest with prebuilt binaries or cargo. Most commands are intended for Rust workspaces, but version and help commands work anywhere."
|
|
258
258
|
},
|
|
259
|
+
mysql: {
|
|
260
|
+
plugin: "mysql",
|
|
261
|
+
binary: "mysql",
|
|
262
|
+
check: "mysql --version",
|
|
263
|
+
install_steps: [
|
|
264
|
+
"mysql --version",
|
|
265
|
+
"supercli plugins install mysql",
|
|
266
|
+
"supercli mysql cli version --json",
|
|
267
|
+
"supercli mysql query execute --execute \"select 1\" --host 127.0.0.1 --user root --database mysql --json"
|
|
268
|
+
],
|
|
269
|
+
note: "Install the MySQL client with your platform package manager. The wrapped query command is tuned for batch output and works best when connection settings come from flags or standard MySQL environment variables like MYSQL_HOST, MYSQL_USER, MYSQL_DATABASE, and MYSQL_PWD."
|
|
270
|
+
},
|
|
271
|
+
mongosh: {
|
|
272
|
+
plugin: "mongosh",
|
|
273
|
+
binary: "mongosh",
|
|
274
|
+
check: "mongosh --version",
|
|
275
|
+
install_steps: [
|
|
276
|
+
"mongosh --version",
|
|
277
|
+
"supercli plugins install mongosh",
|
|
278
|
+
"supercli mongosh cli version --json",
|
|
279
|
+
"supercli mongosh server ping --host 127.0.0.1 --port 27017 --json",
|
|
280
|
+
"supercli mongosh eval run --javascript \"db.adminCommand({ ping: 1 })\" --json"
|
|
281
|
+
],
|
|
282
|
+
note: "This plugin targets the current mongosh shell rather than the legacy mongo shell. Wrapped commands prefer relaxed JSON output for automation, while raw passthrough remains available for connection-string-based flows."
|
|
283
|
+
},
|
|
284
|
+
blogwatcher: {
|
|
285
|
+
plugin: "blogwatcher",
|
|
286
|
+
binary: "blogwatcher",
|
|
287
|
+
check: "blogwatcher --version",
|
|
288
|
+
install_steps: [
|
|
289
|
+
"brew install Hyaxia/tap/blogwatcher",
|
|
290
|
+
"go install github.com/Hyaxia/blogwatcher/cmd/blogwatcher@latest",
|
|
291
|
+
"blogwatcher --version",
|
|
292
|
+
"supercli plugins install blogwatcher",
|
|
293
|
+
"supercli skills list --catalog --provider blogwatcher --json",
|
|
294
|
+
"supercli skills get blogwatcher:root.skill",
|
|
295
|
+
"supercli blogwatcher blogs list --json"
|
|
296
|
+
],
|
|
297
|
+
note: "This hybrid plugin indexes the upstream BlogWatcher README and SKILL documents into the skills catalog and exposes non-interactive wrappers for the local CLI. BlogWatcher stores data under ~/.blogwatcher, so use an isolated HOME when you want disposable test data."
|
|
298
|
+
},
|
|
299
|
+
himalaya: {
|
|
300
|
+
plugin: "himalaya",
|
|
301
|
+
binary: "himalaya",
|
|
302
|
+
check: "himalaya --version",
|
|
303
|
+
install_steps: [
|
|
304
|
+
"brew install himalaya",
|
|
305
|
+
"cargo install himalaya --locked",
|
|
306
|
+
"himalaya --version",
|
|
307
|
+
"supercli plugins install himalaya",
|
|
308
|
+
"supercli himalaya account list --json",
|
|
309
|
+
"supercli himalaya folder list --account personal --json",
|
|
310
|
+
"supercli himalaya envelope list --account personal --folder INBOX --page 1 --json"
|
|
311
|
+
],
|
|
312
|
+
note: "Prefer the wrapped read-only commands for automation. Himalaya itself uses --output json rather than --json, and interactive or write-side flows like account configure, message send, and mailbox mutation are intentionally left out of wrapped v1."
|
|
313
|
+
},
|
|
314
|
+
wacli: {
|
|
315
|
+
plugin: "wacli",
|
|
316
|
+
binary: "wacli",
|
|
317
|
+
check: "wacli --version",
|
|
318
|
+
install_steps: [
|
|
319
|
+
"brew install steipete/tap/wacli",
|
|
320
|
+
"go build -tags sqlite_fts5 -o ./dist/wacli ./cmd/wacli",
|
|
321
|
+
"wacli --version",
|
|
322
|
+
"supercli plugins install wacli",
|
|
323
|
+
"supercli wacli doctor run --json",
|
|
324
|
+
"supercli wacli auth status --json",
|
|
325
|
+
"supercli wacli chats list --store ~/.wacli --json"
|
|
326
|
+
],
|
|
327
|
+
note: "This plugin intentionally wraps only read-only diagnostics and local-store inspection commands. Use the upstream wacli CLI directly for QR auth, sync loops, sending messages, media download, and group/contact mutations."
|
|
328
|
+
},
|
|
329
|
+
xurl: {
|
|
330
|
+
plugin: "xurl",
|
|
331
|
+
binary: "xurl",
|
|
332
|
+
check: "xurl version",
|
|
333
|
+
install_steps: [
|
|
334
|
+
"brew install --cask xdevplatform/tap/xurl",
|
|
335
|
+
"npm install -g @xdevplatform/xurl",
|
|
336
|
+
"go install github.com/xdevplatform/xurl@latest",
|
|
337
|
+
"xurl version",
|
|
338
|
+
"supercli plugins install xurl",
|
|
339
|
+
"supercli skills list --catalog --provider xurl --json",
|
|
340
|
+
"supercli xurl auth status --json",
|
|
341
|
+
"supercli xurl account whoami --json"
|
|
342
|
+
],
|
|
343
|
+
note: "This hybrid plugin indexes upstream xurl docs and exposes a curated set of read-only wrappers. Use the upstream xurl CLI directly for auth setup, posting, likes, follows, raw requests, streams, webhooks, and any command that can mutate X state."
|
|
344
|
+
},
|
|
345
|
+
clix: {
|
|
346
|
+
plugin: "clix",
|
|
347
|
+
binary: "clix",
|
|
348
|
+
check: "clix auth status --json",
|
|
349
|
+
install_steps: [
|
|
350
|
+
"uv pip install clix0",
|
|
351
|
+
"clix auth",
|
|
352
|
+
"supercli plugins install clix",
|
|
353
|
+
"supercli skills list --catalog --provider clix --json",
|
|
354
|
+
"supercli clix auth status --json",
|
|
355
|
+
"supercli clix timeline list --count 10 --json"
|
|
356
|
+
],
|
|
357
|
+
note: "This hybrid plugin indexes upstream clix docs and exposes curated read-only wrappers. Use upstream clix directly for cookie login, account switching, posting, deleting, likes, retweets, and bookmark mutations."
|
|
358
|
+
},
|
|
259
359
|
cline: {
|
|
260
360
|
plugin: "cline",
|
|
261
361
|
binary: "cline",
|