@zenalexa/unicli 0.207.1 → 0.209.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/AGENTS.md +4 -4
- package/README.md +30 -17
- package/dist/browser/observe.d.ts +80 -0
- package/dist/browser/observe.d.ts.map +1 -0
- package/dist/browser/observe.js +144 -0
- package/dist/browser/observe.js.map +1 -0
- package/dist/browser/snapshot.d.ts.map +1 -1
- package/dist/browser/snapshot.js +29 -9
- package/dist/browser/snapshot.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +59 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +82 -2
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/eval.d.ts +112 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +485 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +20 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/hub.d.ts +13 -0
- package/dist/commands/hub.d.ts.map +1 -0
- package/dist/commands/hub.js +232 -0
- package/dist/commands/hub.js.map +1 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +135 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/operate.d.ts.map +1 -1
- package/dist/commands/operate.js +66 -1
- package/dist/commands/operate.js.map +1 -1
- package/dist/commands/research.d.ts +17 -0
- package/dist/commands/research.d.ts.map +1 -0
- package/dist/commands/research.js +257 -0
- package/dist/commands/research.js.map +1 -0
- package/dist/commands/skills.d.ts +91 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +266 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/test-gen.d.ts +10 -0
- package/dist/commands/test-gen.d.ts.map +1 -0
- package/dist/commands/test-gen.js +124 -0
- package/dist/commands/test-gen.js.map +1 -0
- package/dist/commands/usage.d.ts +17 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +87 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/discovery/loader.d.ts +8 -1
- package/dist/discovery/loader.d.ts.map +1 -1
- package/dist/discovery/loader.js +103 -6
- package/dist/discovery/loader.js.map +1 -1
- package/dist/engine/capability.d.ts +40 -0
- package/dist/engine/capability.d.ts.map +1 -0
- package/dist/engine/capability.js +191 -0
- package/dist/engine/capability.js.map +1 -0
- package/dist/engine/endpoint.d.ts +47 -0
- package/dist/engine/endpoint.d.ts.map +1 -0
- package/dist/engine/endpoint.js +295 -0
- package/dist/engine/endpoint.js.map +1 -0
- package/dist/engine/framework.d.ts +28 -0
- package/dist/engine/framework.d.ts.map +1 -0
- package/dist/engine/framework.js +66 -0
- package/dist/engine/framework.js.map +1 -0
- package/dist/engine/probe.d.ts +19 -0
- package/dist/engine/probe.d.ts.map +1 -0
- package/dist/engine/probe.js +85 -0
- package/dist/engine/probe.js.map +1 -0
- package/dist/engine/research.d.ts +38 -0
- package/dist/engine/research.d.ts.map +1 -0
- package/dist/engine/research.js +414 -0
- package/dist/engine/research.js.map +1 -0
- package/dist/engine/update-check.js +1 -1
- package/dist/engine/update-check.js.map +1 -1
- package/dist/engine/yaml-runner.d.ts.map +1 -1
- package/dist/engine/yaml-runner.js +115 -6
- package/dist/engine/yaml-runner.js.map +1 -1
- package/dist/main.d.ts +1 -4
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -4
- package/dist/main.js.map +1 -1
- package/dist/manifest.json +519 -1
- package/dist/mcp/server.d.ts +19 -6
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +459 -115
- package/dist/mcp/server.js.map +1 -1
- package/dist/permissions/sensitive-paths.d.ts +92 -0
- package/dist/permissions/sensitive-paths.d.ts.map +1 -0
- package/dist/permissions/sensitive-paths.js +174 -0
- package/dist/permissions/sensitive-paths.js.map +1 -0
- package/dist/runtime/usage-ledger.d.ts +86 -0
- package/dist/runtime/usage-ledger.d.ts.map +1 -0
- package/dist/runtime/usage-ledger.js +173 -0
- package/dist/runtime/usage-ledger.js.map +1 -0
- package/package.json +8 -7
- package/src/adapters/autoagent/eval-run.yaml +36 -0
- package/src/adapters/cnn/top.yaml +21 -0
- package/src/adapters/cocoapods/search.yaml +16 -0
- package/src/adapters/crates-io/search.yaml +27 -0
- package/src/adapters/cua/bench-list.yaml +20 -0
- package/src/adapters/cua/bench-run.yaml +32 -0
- package/src/adapters/docker-hub/search.yaml +26 -0
- package/src/adapters/eastmoney/hot.yaml +23 -0
- package/src/adapters/eastmoney/search.yaml +25 -0
- package/src/adapters/exchangerate/convert.yaml +19 -0
- package/src/adapters/feishu/calendar.yaml +24 -0
- package/src/adapters/feishu/docs.yaml +17 -0
- package/src/adapters/feishu/send.yaml +29 -0
- package/src/adapters/feishu/tasks.yaml +24 -0
- package/src/adapters/gitee/search.yaml +25 -0
- package/src/adapters/gitee/trending.yaml +22 -0
- package/src/adapters/gitlab/search.yaml +24 -0
- package/src/adapters/gitlab/trending.yaml +22 -0
- package/src/adapters/godot/project-run.yaml +31 -0
- package/src/adapters/godot/scene-export.yaml +39 -0
- package/src/adapters/hermes/sessions-search.yaml +61 -0
- package/src/adapters/hermes/skills-list.yaml +30 -0
- package/src/adapters/hermes/skills-read.yaml +46 -0
- package/src/adapters/homebrew/info.yaml +15 -0
- package/src/adapters/huggingface-papers/daily.yaml +21 -0
- package/src/adapters/infoq/articles.yaml +29 -0
- package/src/adapters/ip-info/lookup.yaml +15 -0
- package/src/adapters/itch-io/popular.yaml +22 -0
- package/src/adapters/ithome/news.yaml +21 -0
- package/src/adapters/mastodon/search.yaml +29 -0
- package/src/adapters/mastodon/trending.yaml +27 -0
- package/src/adapters/meituan/search.yaml +30 -0
- package/src/adapters/minimax/chat.yaml +33 -0
- package/src/adapters/minimax/models.yaml +18 -0
- package/src/adapters/minimax/tts.yaml +33 -0
- package/src/adapters/motion-studio/component-get.yaml +35 -0
- package/src/adapters/netease-music/hot.yaml +24 -0
- package/src/adapters/netease-music/search.yaml +29 -0
- package/src/adapters/npm-trends/compare.yaml +19 -0
- package/src/adapters/nytimes/top.yaml +26 -0
- package/src/adapters/openharness/memory-read.yaml +51 -0
- package/src/adapters/openharness/skills-list.yaml +28 -0
- package/src/adapters/openrouter/models.yaml +22 -0
- package/src/adapters/pexels/search.yaml +28 -0
- package/src/adapters/pinduoduo/hot.yaml +20 -0
- package/src/adapters/pypi/info.yaml +16 -0
- package/src/adapters/qweather/now.yaml +16 -0
- package/src/adapters/renderdoc/capture-list.yaml +42 -0
- package/src/adapters/renderdoc/frame-export.yaml +32 -0
- package/src/adapters/replicate/search.yaml +25 -0
- package/src/adapters/replicate/trending.yaml +22 -0
- package/src/adapters/sspai/hot.yaml +21 -0
- package/src/adapters/sspai/latest.yaml +22 -0
- package/src/adapters/stagehand/wrap-observe.yaml +42 -0
- package/src/adapters/techcrunch/latest.yaml +22 -0
- package/src/adapters/theverge/latest.yaml +21 -0
- package/src/adapters/twitch/top.yaml +26 -0
- package/src/adapters/unsplash/search.yaml +28 -0
- package/src/adapters/ycombinator/launches.yaml +20 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP (Model Context Protocol)
|
|
3
|
+
* MCP (Model Context Protocol) server for Uni-CLI.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Two registration modes:
|
|
6
|
+
* 1. **Expanded (default)** — one tool per adapter command
|
|
7
|
+
* (`unicli_<site>_<command>`) with JSON Schema derived from `args` +
|
|
8
|
+
* `columns`. This is the production mode the v0.208 plan calls out:
|
|
9
|
+
* MCP clients see the full Uni-CLI surface area without an extra
|
|
10
|
+
* list_adapters → run_command roundtrip.
|
|
11
|
+
* 2. **Lazy (`--lazy`)** — only `list_adapters` + `run_command` are
|
|
12
|
+
* registered. Useful when an MCP client has a hard tool-count limit
|
|
13
|
+
* or wants the smallest possible handshake.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
15
|
+
* Two transports:
|
|
16
|
+
* - **stdio (default)** — newline-delimited JSON over stdin/stdout
|
|
17
|
+
* - **http (`--transport http [--port 19826]`)** — POST /mcp accepts a
|
|
18
|
+
* single JSON-RPC envelope and returns a single JSON response. No
|
|
19
|
+
* SSE streaming yet — additive in a future release.
|
|
20
|
+
*
|
|
21
|
+
* Auth pass-through is automatic: every adapter the CLI loads (including
|
|
22
|
+
* cookie-based ones) is exposed by name; the runtime resolves cookies on
|
|
23
|
+
* each call via the same code path as the CLI.
|
|
11
24
|
*/
|
|
12
25
|
import { createInterface } from "node:readline";
|
|
26
|
+
import { createServer, } from "node:http";
|
|
13
27
|
import { loadAllAdapters, loadTsAdapters } from "../discovery/loader.js";
|
|
14
28
|
import { getAllAdapters, listCommands, resolveCommand } from "../registry.js";
|
|
15
29
|
import { runPipeline } from "../engine/yaml-runner.js";
|
|
16
30
|
import { VERSION } from "../constants.js";
|
|
17
|
-
// ──
|
|
18
|
-
function
|
|
31
|
+
// ── Lazy-mode core tools (preserved for `--lazy` flag) ──────────────────────
|
|
32
|
+
function buildLazyTools() {
|
|
19
33
|
return [
|
|
20
34
|
{
|
|
21
35
|
name: "list_adapters",
|
|
@@ -62,6 +76,161 @@ function buildCoreTools() {
|
|
|
62
76
|
},
|
|
63
77
|
];
|
|
64
78
|
}
|
|
79
|
+
// ── Expanded-mode: one tool per adapter command ─────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Map an adapter `arg.type` to a JSON Schema primitive. Defaults to "string"
|
|
82
|
+
* for unknown / missing types — safer than failing the schema build.
|
|
83
|
+
*/
|
|
84
|
+
function jsonTypeFor(t) {
|
|
85
|
+
switch (t) {
|
|
86
|
+
case "int":
|
|
87
|
+
return "integer";
|
|
88
|
+
case "float":
|
|
89
|
+
return "number";
|
|
90
|
+
case "bool":
|
|
91
|
+
return "boolean";
|
|
92
|
+
case "str":
|
|
93
|
+
default:
|
|
94
|
+
return "string";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build the input JSON Schema for one adapter command from its `args`.
|
|
99
|
+
*/
|
|
100
|
+
function buildInputSchema(cmd) {
|
|
101
|
+
const props = {
|
|
102
|
+
limit: {
|
|
103
|
+
type: "integer",
|
|
104
|
+
description: "Cap result count (default 20)",
|
|
105
|
+
default: 20,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const required = [];
|
|
109
|
+
for (const a of cmd.adapterArgs ?? []) {
|
|
110
|
+
if (a.name === "limit")
|
|
111
|
+
continue; // already added
|
|
112
|
+
const prop = {
|
|
113
|
+
type: jsonTypeFor(a.type),
|
|
114
|
+
description: a.description,
|
|
115
|
+
};
|
|
116
|
+
if (a.default !== undefined)
|
|
117
|
+
prop.default = a.default;
|
|
118
|
+
if (a.choices)
|
|
119
|
+
prop.enum = a.choices;
|
|
120
|
+
props[a.name] = prop;
|
|
121
|
+
if (a.required)
|
|
122
|
+
required.push(a.name);
|
|
123
|
+
}
|
|
124
|
+
const schema = {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: props,
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
};
|
|
129
|
+
if (required.length > 0)
|
|
130
|
+
schema.required = required;
|
|
131
|
+
return schema;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build the output JSON Schema. We model results as `{ count, results }`
|
|
135
|
+
* mirroring run_command, where each item in `results` follows the
|
|
136
|
+
* `columns` shape (string-typed properties — Uni-CLI columns are
|
|
137
|
+
* format-agnostic and the runtime emits whatever the pipeline produced).
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* Build an output JSON Schema. We model results as `{count, results}` where
|
|
141
|
+
* `results` is an array of items. `columns` becomes the item's property set.
|
|
142
|
+
*
|
|
143
|
+
* Note: we return a simple nested schema rather than a full JSON Schema
|
|
144
|
+
* (which would need a deeper `items` type for `array`). Most MCP clients
|
|
145
|
+
* only inspect the top-level type; Anthropic's client is permissive. If a
|
|
146
|
+
* strict validator rejects this, it will still fall back to the lazy tool
|
|
147
|
+
* path via `run_command`.
|
|
148
|
+
*/
|
|
149
|
+
function buildOutputSchema(cmd) {
|
|
150
|
+
const itemProps = {};
|
|
151
|
+
for (const col of cmd.columns ?? []) {
|
|
152
|
+
itemProps[col] = { type: "string", description: `Column: ${col}` };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
count: { type: "integer", description: "Number of results returned" },
|
|
158
|
+
results: {
|
|
159
|
+
type: "array",
|
|
160
|
+
description: "Result rows",
|
|
161
|
+
items: {
|
|
162
|
+
type: "object",
|
|
163
|
+
...(Object.keys(itemProps).length > 0
|
|
164
|
+
? { properties: itemProps }
|
|
165
|
+
: {}),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* MCP tool name: `unicli_<site>_<command>` with non-alphanumeric chars
|
|
173
|
+
* collapsed to `_`. Anthropic / Claude Desktop accept underscores; some
|
|
174
|
+
* older clients reject hyphens, so we normalize defensively.
|
|
175
|
+
*
|
|
176
|
+
* CRITICAL: normalization is NOT reversible (e.g. both `claude-code_version`
|
|
177
|
+
* and `claude_code-version` would yield the same normalized name). The
|
|
178
|
+
* expanded-mode dispatcher uses a name → {adapter, cmdName} lookup table
|
|
179
|
+
* built at the same time as the tool list, so callers never need to reverse
|
|
180
|
+
* the normalization. See `expandedRegistry` and `buildExpandedTools` below.
|
|
181
|
+
*/
|
|
182
|
+
function buildToolName(site, command) {
|
|
183
|
+
return `unicli_${site}_${command}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
184
|
+
}
|
|
185
|
+
const expandedRegistry = new Map();
|
|
186
|
+
function buildExpandedTools() {
|
|
187
|
+
const tools = [];
|
|
188
|
+
// Always include list_adapters for discovery + as a smoke test.
|
|
189
|
+
tools.push(buildLazyTools()[0]);
|
|
190
|
+
expandedRegistry.clear();
|
|
191
|
+
// Collision detection: if two (site, command) pairs normalize to the same
|
|
192
|
+
// tool name, the first one wins and the second is silently shadowed. We
|
|
193
|
+
// don't expect this in practice (most adapters use lowercase alphanumeric
|
|
194
|
+
// + hyphen names), but flag it on stderr so it gets noticed.
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
for (const adapter of getAllAdapters()) {
|
|
197
|
+
for (const [cmdName, cmd] of Object.entries(adapter.commands)) {
|
|
198
|
+
const description = cmd.description?.trim() ||
|
|
199
|
+
adapter.description?.trim() ||
|
|
200
|
+
`${cmdName} for ${adapter.name}`;
|
|
201
|
+
const toolName = buildToolName(adapter.name, cmdName);
|
|
202
|
+
if (seen.has(toolName)) {
|
|
203
|
+
process.stderr.write(`unicli MCP: tool name collision: ${toolName} — shadowing ${adapter.name}/${cmdName}\n`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
seen.add(toolName);
|
|
207
|
+
expandedRegistry.set(toolName, { adapter, cmdName, cmd });
|
|
208
|
+
tools.push({
|
|
209
|
+
name: toolName,
|
|
210
|
+
description: `[${adapter.name}] ${description}`,
|
|
211
|
+
inputSchema: buildInputSchema(cmd),
|
|
212
|
+
outputSchema: buildOutputSchema(cmd),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Add unicli_discover tool — expose explore+synthesize+generate as MCP tool
|
|
217
|
+
tools.push({
|
|
218
|
+
name: "unicli_discover",
|
|
219
|
+
description: "Auto-discover CLI capabilities for any website URL. Navigates the page, captures API endpoints, and optionally generates YAML adapters.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
url: { type: "string", description: "Website URL to explore" },
|
|
224
|
+
goal: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "Optional: capability to find (e.g. 'search', 'hot', 'feed')",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ["url"],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
return tools;
|
|
233
|
+
}
|
|
65
234
|
// ── Tool Handlers ───────────────────────────────────────────────────────────
|
|
66
235
|
function handleListAdapters(params) {
|
|
67
236
|
let commands = listCommands();
|
|
@@ -102,47 +271,7 @@ function handleListAdapters(params) {
|
|
|
102
271
|
],
|
|
103
272
|
};
|
|
104
273
|
}
|
|
105
|
-
async function
|
|
106
|
-
const site = params.site;
|
|
107
|
-
const command = params.command;
|
|
108
|
-
const args = params.args ?? {};
|
|
109
|
-
if (!site || !command) {
|
|
110
|
-
return {
|
|
111
|
-
content: [
|
|
112
|
-
{
|
|
113
|
-
type: "text",
|
|
114
|
-
text: JSON.stringify({ error: "site and command are required" }),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
isError: true,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const resolved = resolveCommand(site, command);
|
|
121
|
-
if (!resolved) {
|
|
122
|
-
// Provide helpful suggestion
|
|
123
|
-
const adapters = getAllAdapters();
|
|
124
|
-
const matchingSites = adapters
|
|
125
|
-
.filter((a) => a.name.includes(site))
|
|
126
|
-
.map((a) => ({
|
|
127
|
-
site: a.name,
|
|
128
|
-
commands: Object.keys(a.commands),
|
|
129
|
-
}));
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{
|
|
133
|
-
type: "text",
|
|
134
|
-
text: JSON.stringify({
|
|
135
|
-
error: `Unknown command: ${site} ${command}`,
|
|
136
|
-
suggestion: matchingSites.length > 0
|
|
137
|
-
? `Did you mean one of these? ${JSON.stringify(matchingSites)}`
|
|
138
|
-
: "Use list_adapters to see all available commands.",
|
|
139
|
-
}, null, 2),
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
isError: true,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
const { adapter, command: cmd } = resolved;
|
|
274
|
+
async function runResolvedCommand(adapter, cmd, cmdName, args) {
|
|
146
275
|
// Merge default args
|
|
147
276
|
const mergedArgs = { limit: 20, ...args };
|
|
148
277
|
if (args.limit !== undefined) {
|
|
@@ -193,7 +322,7 @@ async function handleRunCommand(params) {
|
|
|
193
322
|
type: "text",
|
|
194
323
|
text: JSON.stringify({
|
|
195
324
|
error: message,
|
|
196
|
-
adapter_path: `src/adapters/${adapter.name}/${
|
|
325
|
+
adapter_path: `src/adapters/${adapter.name}/${cmdName}.yaml`,
|
|
197
326
|
suggestion: "The adapter may need updating. Check the YAML file.",
|
|
198
327
|
}, null, 2),
|
|
199
328
|
},
|
|
@@ -202,91 +331,221 @@ async function handleRunCommand(params) {
|
|
|
202
331
|
};
|
|
203
332
|
}
|
|
204
333
|
}
|
|
334
|
+
async function handleRunCommand(params) {
|
|
335
|
+
const site = params.site;
|
|
336
|
+
const command = params.command;
|
|
337
|
+
const args = params.args ?? {};
|
|
338
|
+
if (!site || !command) {
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: JSON.stringify({ error: "site and command are required" }),
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
isError: true,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const resolved = resolveCommand(site, command);
|
|
350
|
+
if (!resolved) {
|
|
351
|
+
const adapters = getAllAdapters();
|
|
352
|
+
const matchingSites = adapters
|
|
353
|
+
.filter((a) => a.name.includes(site))
|
|
354
|
+
.map((a) => ({
|
|
355
|
+
site: a.name,
|
|
356
|
+
commands: Object.keys(a.commands),
|
|
357
|
+
}));
|
|
358
|
+
return {
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: JSON.stringify({
|
|
363
|
+
error: `Unknown command: ${site} ${command}`,
|
|
364
|
+
suggestion: matchingSites.length > 0
|
|
365
|
+
? `Did you mean one of these? ${JSON.stringify(matchingSites)}`
|
|
366
|
+
: "Use list_adapters to see all available commands.",
|
|
367
|
+
}, null, 2),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
isError: true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return runResolvedCommand(resolved.adapter, resolved.command, command, args);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Expanded-tool dispatcher — parse `unicli_<site>_<command>` back to its
|
|
377
|
+
* components and call the resolver. Returns `undefined` when the tool name
|
|
378
|
+
* is not in expanded form, so the caller can fall through to lazy-tool
|
|
379
|
+
* handling (list_adapters / run_command).
|
|
380
|
+
*/
|
|
381
|
+
async function handleExpandedTool(toolName, args) {
|
|
382
|
+
if (!toolName.startsWith("unicli_"))
|
|
383
|
+
return undefined;
|
|
384
|
+
// Dictionary lookup into the expansion registry built by
|
|
385
|
+
// `buildExpandedTools`. This is the ONLY correct way to map a normalized
|
|
386
|
+
// tool name back to its adapter + command because the normalization
|
|
387
|
+
// (`s/[^a-zA-Z0-9_]/_/g`) is not reversible — a command file named
|
|
388
|
+
// `capture-list.yaml` and another named `capture_list.yaml` would map
|
|
389
|
+
// to the same tool name. The registry resolves the ambiguity deterministically
|
|
390
|
+
// (first-write-wins, collisions logged to stderr in `buildExpandedTools`).
|
|
391
|
+
const entry = expandedRegistry.get(toolName);
|
|
392
|
+
if (!entry)
|
|
393
|
+
return undefined;
|
|
394
|
+
return runResolvedCommand(entry.adapter, entry.cmd, entry.cmdName, args);
|
|
395
|
+
}
|
|
205
396
|
// ── MCP Protocol Handler ────────────────────────────────────────────────────
|
|
206
397
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
398
|
+
function parseArgs(argv) {
|
|
399
|
+
const opts = {
|
|
400
|
+
lazy: false,
|
|
401
|
+
transport: "stdio",
|
|
402
|
+
port: 19826,
|
|
403
|
+
};
|
|
404
|
+
for (let i = 0; i < argv.length; i++) {
|
|
405
|
+
const a = argv[i];
|
|
406
|
+
if (a === "--lazy")
|
|
407
|
+
opts.lazy = true;
|
|
408
|
+
else if (a === "--transport") {
|
|
409
|
+
const v = argv[++i];
|
|
410
|
+
if (v === "stdio" || v === "http")
|
|
411
|
+
opts.transport = v;
|
|
412
|
+
}
|
|
413
|
+
else if (a === "--port") {
|
|
414
|
+
const v = parseInt(argv[++i], 10);
|
|
415
|
+
if (Number.isFinite(v))
|
|
416
|
+
opts.port = v;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return opts;
|
|
420
|
+
}
|
|
421
|
+
function buildHandler(tools) {
|
|
422
|
+
return function handleRequest(req) {
|
|
423
|
+
const id = req.id ?? null;
|
|
424
|
+
switch (req.method) {
|
|
425
|
+
case "initialize":
|
|
426
|
+
return {
|
|
427
|
+
jsonrpc: "2.0",
|
|
428
|
+
id,
|
|
429
|
+
result: {
|
|
430
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
431
|
+
capabilities: {
|
|
432
|
+
tools: {},
|
|
433
|
+
},
|
|
434
|
+
serverInfo: {
|
|
435
|
+
name: "unicli",
|
|
436
|
+
version: VERSION,
|
|
437
|
+
},
|
|
223
438
|
},
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return null;
|
|
229
|
-
case "tools/list":
|
|
230
|
-
return {
|
|
231
|
-
jsonrpc: "2.0",
|
|
232
|
-
id,
|
|
233
|
-
result: { tools },
|
|
234
|
-
};
|
|
235
|
-
case "tools/call": {
|
|
236
|
-
const params = req.params;
|
|
237
|
-
if (!params?.name) {
|
|
439
|
+
};
|
|
440
|
+
case "notifications/initialized":
|
|
441
|
+
return null;
|
|
442
|
+
case "tools/list":
|
|
238
443
|
return {
|
|
239
444
|
jsonrpc: "2.0",
|
|
240
445
|
id,
|
|
241
|
-
|
|
446
|
+
result: { tools },
|
|
242
447
|
};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const result = handleListAdapters(toolArgs);
|
|
248
|
-
return { jsonrpc: "2.0", id, result };
|
|
249
|
-
}
|
|
250
|
-
case "run_command":
|
|
251
|
-
return handleRunCommand(toolArgs).then((result) => ({
|
|
448
|
+
case "tools/call": {
|
|
449
|
+
const params = req.params;
|
|
450
|
+
if (!params?.name) {
|
|
451
|
+
return {
|
|
252
452
|
jsonrpc: "2.0",
|
|
253
453
|
id,
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
454
|
+
error: { code: -32602, message: "Missing tool name" },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const toolArgs = params.arguments ?? {};
|
|
458
|
+
switch (params.name) {
|
|
459
|
+
case "list_adapters": {
|
|
460
|
+
const result = handleListAdapters(toolArgs);
|
|
461
|
+
return { jsonrpc: "2.0", id, result };
|
|
462
|
+
}
|
|
463
|
+
case "run_command":
|
|
464
|
+
return handleRunCommand(toolArgs).then((result) => ({
|
|
465
|
+
jsonrpc: "2.0",
|
|
466
|
+
id,
|
|
467
|
+
result,
|
|
468
|
+
}));
|
|
469
|
+
case "unicli_discover": {
|
|
470
|
+
const discoverUrl = toolArgs.url;
|
|
471
|
+
const discoverGoal = toolArgs.goal;
|
|
472
|
+
if (!discoverUrl) {
|
|
473
|
+
return {
|
|
474
|
+
jsonrpc: "2.0",
|
|
475
|
+
id,
|
|
476
|
+
error: {
|
|
477
|
+
code: -32602,
|
|
478
|
+
message: "Missing required parameter: url",
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return import("node:child_process").then(({ execFile: ef }) => import("node:util").then(({ promisify: prom }) => {
|
|
483
|
+
const execFileP = prom(ef);
|
|
484
|
+
const discoverArgs = ["generate", discoverUrl, "--json"];
|
|
485
|
+
if (discoverGoal)
|
|
486
|
+
discoverArgs.push("--goal", discoverGoal);
|
|
487
|
+
return execFileP("unicli", discoverArgs, {
|
|
488
|
+
timeout: 120_000,
|
|
489
|
+
encoding: "utf-8",
|
|
490
|
+
}).then(({ stdout }) => ({
|
|
491
|
+
jsonrpc: "2.0",
|
|
492
|
+
id,
|
|
493
|
+
result: { content: [{ type: "text", text: stdout }] },
|
|
494
|
+
}), (err) => ({
|
|
495
|
+
jsonrpc: "2.0",
|
|
496
|
+
id,
|
|
497
|
+
result: {
|
|
498
|
+
content: [
|
|
499
|
+
{
|
|
500
|
+
type: "text",
|
|
501
|
+
text: JSON.stringify({
|
|
502
|
+
error: err instanceof Error ? err.message : String(err),
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
isError: true,
|
|
507
|
+
},
|
|
508
|
+
}));
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
default:
|
|
512
|
+
return handleExpandedTool(params.name, toolArgs).then((result) => {
|
|
513
|
+
if (result)
|
|
514
|
+
return { jsonrpc: "2.0", id, result };
|
|
515
|
+
return {
|
|
516
|
+
jsonrpc: "2.0",
|
|
517
|
+
id,
|
|
518
|
+
error: {
|
|
519
|
+
code: -32601,
|
|
520
|
+
message: `Unknown tool: ${params.name}. Use list_adapters to see all available commands.`,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
case "ping":
|
|
527
|
+
return { jsonrpc: "2.0", id, result: {} };
|
|
528
|
+
default:
|
|
529
|
+
if (id !== null && id !== undefined) {
|
|
257
530
|
return {
|
|
258
531
|
jsonrpc: "2.0",
|
|
259
532
|
id,
|
|
260
533
|
error: {
|
|
261
534
|
code: -32601,
|
|
262
|
-
message: `
|
|
535
|
+
message: `Method not found: ${req.method}`,
|
|
263
536
|
},
|
|
264
537
|
};
|
|
265
|
-
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
266
540
|
}
|
|
267
|
-
|
|
268
|
-
return { jsonrpc: "2.0", id, result: {} };
|
|
269
|
-
default:
|
|
270
|
-
// Unknown method — return error for requests (has id), ignore notifications
|
|
271
|
-
if (id !== null && id !== undefined) {
|
|
272
|
-
return {
|
|
273
|
-
jsonrpc: "2.0",
|
|
274
|
-
id,
|
|
275
|
-
error: { code: -32601, message: `Method not found: ${req.method}` },
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
541
|
+
};
|
|
280
542
|
}
|
|
281
543
|
// ── Stdio Transport ─────────────────────────────────────────────────────────
|
|
282
544
|
function send(response) {
|
|
283
545
|
const json = JSON.stringify(response);
|
|
284
546
|
process.stdout.write(json + "\n");
|
|
285
547
|
}
|
|
286
|
-
async function
|
|
287
|
-
// Load adapters (same as CLI)
|
|
288
|
-
loadAllAdapters();
|
|
289
|
-
await loadTsAdapters();
|
|
548
|
+
async function startStdio(handler) {
|
|
290
549
|
const rl = createInterface({
|
|
291
550
|
input: process.stdin,
|
|
292
551
|
terminal: false,
|
|
@@ -308,7 +567,7 @@ async function main() {
|
|
|
308
567
|
return;
|
|
309
568
|
}
|
|
310
569
|
try {
|
|
311
|
-
const response = await
|
|
570
|
+
const response = await handler(req);
|
|
312
571
|
if (response) {
|
|
313
572
|
send(response);
|
|
314
573
|
}
|
|
@@ -325,10 +584,95 @@ async function main() {
|
|
|
325
584
|
rl.on("close", () => {
|
|
326
585
|
process.exit(0);
|
|
327
586
|
});
|
|
328
|
-
|
|
587
|
+
}
|
|
588
|
+
// ── HTTP Transport ──────────────────────────────────────────────────────────
|
|
589
|
+
/**
|
|
590
|
+
* Simple JSON-RPC over HTTP. POST /mcp accepts a single JSON-RPC envelope and
|
|
591
|
+
* returns a single JSON response. GET /mcp returns server info — handy for
|
|
592
|
+
* a health check from a browser.
|
|
593
|
+
*
|
|
594
|
+
* Note: this is intentionally NOT a full MCP Streamable HTTP transport —
|
|
595
|
+
* no SSE event stream, no session resume. Most clients that "speak HTTP"
|
|
596
|
+
* to MCP only need request/response, and starting with the simpler shape
|
|
597
|
+
* means zero new dependencies and a tiny attack surface.
|
|
598
|
+
*/
|
|
599
|
+
async function startHttp(handler, port, toolCount) {
|
|
600
|
+
const server = createServer((req, res) => {
|
|
601
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "/mcp")) {
|
|
602
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
603
|
+
res.end(JSON.stringify({
|
|
604
|
+
server: "unicli",
|
|
605
|
+
version: VERSION,
|
|
606
|
+
tools: toolCount,
|
|
607
|
+
protocol: PROTOCOL_VERSION,
|
|
608
|
+
}));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (req.method !== "POST" || req.url !== "/mcp") {
|
|
612
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
613
|
+
res.end(JSON.stringify({ error: "POST /mcp" }));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const chunks = [];
|
|
617
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
618
|
+
req.on("end", async () => {
|
|
619
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
620
|
+
let parsed;
|
|
621
|
+
try {
|
|
622
|
+
parsed = JSON.parse(body);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
626
|
+
res.end(JSON.stringify({
|
|
627
|
+
jsonrpc: "2.0",
|
|
628
|
+
id: null,
|
|
629
|
+
error: { code: -32700, message: "Parse error" },
|
|
630
|
+
}));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const response = await handler(parsed);
|
|
635
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
636
|
+
res.end(JSON.stringify(response ?? null));
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
640
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
641
|
+
res.end(JSON.stringify({
|
|
642
|
+
jsonrpc: "2.0",
|
|
643
|
+
id: parsed.id ?? null,
|
|
644
|
+
error: { code: -32603, message: `Internal error: ${message}` },
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
await new Promise((resolve, reject) => {
|
|
650
|
+
server.once("error", reject);
|
|
651
|
+
server.listen(port, "127.0.0.1", () => {
|
|
652
|
+
server.off("error", reject);
|
|
653
|
+
resolve();
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
process.stderr.write(`unicli MCP server v${VERSION} — HTTP transport on http://127.0.0.1:${port}/mcp\n`);
|
|
657
|
+
}
|
|
658
|
+
// ── main ────────────────────────────────────────────────────────────────────
|
|
659
|
+
async function main() {
|
|
660
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
661
|
+
// Load adapters (same as CLI)
|
|
662
|
+
loadAllAdapters();
|
|
663
|
+
await loadTsAdapters();
|
|
664
|
+
const tools = opts.lazy ? buildLazyTools() : buildExpandedTools();
|
|
665
|
+
const handler = buildHandler(tools);
|
|
329
666
|
const adapterCount = getAllAdapters().length;
|
|
330
667
|
const commandCount = listCommands().length;
|
|
331
|
-
|
|
668
|
+
if (opts.transport === "http") {
|
|
669
|
+
await startHttp(handler, opts.port, tools.length);
|
|
670
|
+
process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools registered, mode=${opts.lazy ? "lazy" : "expanded"})\n`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// stdio (default)
|
|
674
|
+
await startStdio(handler);
|
|
675
|
+
process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools registered, mode=${opts.lazy ? "lazy" : "expanded"})\n`);
|
|
332
676
|
}
|
|
333
677
|
main().catch((err) => {
|
|
334
678
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|