@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.
Files changed (155) hide show
  1. package/AGENTS.md +4 -4
  2. package/README.md +30 -17
  3. package/dist/browser/observe.d.ts +80 -0
  4. package/dist/browser/observe.d.ts.map +1 -0
  5. package/dist/browser/observe.js +144 -0
  6. package/dist/browser/observe.js.map +1 -0
  7. package/dist/browser/snapshot.d.ts.map +1 -1
  8. package/dist/browser/snapshot.js +29 -9
  9. package/dist/browser/snapshot.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +59 -2
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/agents.d.ts.map +1 -1
  14. package/dist/commands/agents.js +82 -2
  15. package/dist/commands/agents.js.map +1 -1
  16. package/dist/commands/eval.d.ts +112 -0
  17. package/dist/commands/eval.d.ts.map +1 -0
  18. package/dist/commands/eval.js +485 -0
  19. package/dist/commands/eval.js.map +1 -0
  20. package/dist/commands/generate.d.ts.map +1 -1
  21. package/dist/commands/generate.js +20 -1
  22. package/dist/commands/generate.js.map +1 -1
  23. package/dist/commands/hub.d.ts +13 -0
  24. package/dist/commands/hub.d.ts.map +1 -0
  25. package/dist/commands/hub.js +232 -0
  26. package/dist/commands/hub.js.map +1 -0
  27. package/dist/commands/mcp.d.ts +16 -0
  28. package/dist/commands/mcp.d.ts.map +1 -0
  29. package/dist/commands/mcp.js +135 -0
  30. package/dist/commands/mcp.js.map +1 -0
  31. package/dist/commands/operate.d.ts.map +1 -1
  32. package/dist/commands/operate.js +66 -1
  33. package/dist/commands/operate.js.map +1 -1
  34. package/dist/commands/research.d.ts +17 -0
  35. package/dist/commands/research.d.ts.map +1 -0
  36. package/dist/commands/research.js +257 -0
  37. package/dist/commands/research.js.map +1 -0
  38. package/dist/commands/skills.d.ts +91 -0
  39. package/dist/commands/skills.d.ts.map +1 -0
  40. package/dist/commands/skills.js +266 -0
  41. package/dist/commands/skills.js.map +1 -0
  42. package/dist/commands/test-gen.d.ts +10 -0
  43. package/dist/commands/test-gen.d.ts.map +1 -0
  44. package/dist/commands/test-gen.js +124 -0
  45. package/dist/commands/test-gen.js.map +1 -0
  46. package/dist/commands/usage.d.ts +17 -0
  47. package/dist/commands/usage.d.ts.map +1 -0
  48. package/dist/commands/usage.js +87 -0
  49. package/dist/commands/usage.js.map +1 -0
  50. package/dist/discovery/loader.d.ts +8 -1
  51. package/dist/discovery/loader.d.ts.map +1 -1
  52. package/dist/discovery/loader.js +103 -6
  53. package/dist/discovery/loader.js.map +1 -1
  54. package/dist/engine/capability.d.ts +40 -0
  55. package/dist/engine/capability.d.ts.map +1 -0
  56. package/dist/engine/capability.js +191 -0
  57. package/dist/engine/capability.js.map +1 -0
  58. package/dist/engine/endpoint.d.ts +47 -0
  59. package/dist/engine/endpoint.d.ts.map +1 -0
  60. package/dist/engine/endpoint.js +295 -0
  61. package/dist/engine/endpoint.js.map +1 -0
  62. package/dist/engine/framework.d.ts +28 -0
  63. package/dist/engine/framework.d.ts.map +1 -0
  64. package/dist/engine/framework.js +66 -0
  65. package/dist/engine/framework.js.map +1 -0
  66. package/dist/engine/probe.d.ts +19 -0
  67. package/dist/engine/probe.d.ts.map +1 -0
  68. package/dist/engine/probe.js +85 -0
  69. package/dist/engine/probe.js.map +1 -0
  70. package/dist/engine/research.d.ts +38 -0
  71. package/dist/engine/research.d.ts.map +1 -0
  72. package/dist/engine/research.js +414 -0
  73. package/dist/engine/research.js.map +1 -0
  74. package/dist/engine/update-check.js +1 -1
  75. package/dist/engine/update-check.js.map +1 -1
  76. package/dist/engine/yaml-runner.d.ts.map +1 -1
  77. package/dist/engine/yaml-runner.js +115 -6
  78. package/dist/engine/yaml-runner.js.map +1 -1
  79. package/dist/main.d.ts +1 -4
  80. package/dist/main.d.ts.map +1 -1
  81. package/dist/main.js +1 -4
  82. package/dist/main.js.map +1 -1
  83. package/dist/manifest.json +519 -1
  84. package/dist/mcp/server.d.ts +19 -6
  85. package/dist/mcp/server.d.ts.map +1 -1
  86. package/dist/mcp/server.js +459 -115
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/permissions/sensitive-paths.d.ts +92 -0
  89. package/dist/permissions/sensitive-paths.d.ts.map +1 -0
  90. package/dist/permissions/sensitive-paths.js +174 -0
  91. package/dist/permissions/sensitive-paths.js.map +1 -0
  92. package/dist/runtime/usage-ledger.d.ts +86 -0
  93. package/dist/runtime/usage-ledger.d.ts.map +1 -0
  94. package/dist/runtime/usage-ledger.js +173 -0
  95. package/dist/runtime/usage-ledger.js.map +1 -0
  96. package/package.json +8 -7
  97. package/src/adapters/autoagent/eval-run.yaml +36 -0
  98. package/src/adapters/cnn/top.yaml +21 -0
  99. package/src/adapters/cocoapods/search.yaml +16 -0
  100. package/src/adapters/crates-io/search.yaml +27 -0
  101. package/src/adapters/cua/bench-list.yaml +20 -0
  102. package/src/adapters/cua/bench-run.yaml +32 -0
  103. package/src/adapters/docker-hub/search.yaml +26 -0
  104. package/src/adapters/eastmoney/hot.yaml +23 -0
  105. package/src/adapters/eastmoney/search.yaml +25 -0
  106. package/src/adapters/exchangerate/convert.yaml +19 -0
  107. package/src/adapters/feishu/calendar.yaml +24 -0
  108. package/src/adapters/feishu/docs.yaml +17 -0
  109. package/src/adapters/feishu/send.yaml +29 -0
  110. package/src/adapters/feishu/tasks.yaml +24 -0
  111. package/src/adapters/gitee/search.yaml +25 -0
  112. package/src/adapters/gitee/trending.yaml +22 -0
  113. package/src/adapters/gitlab/search.yaml +24 -0
  114. package/src/adapters/gitlab/trending.yaml +22 -0
  115. package/src/adapters/godot/project-run.yaml +31 -0
  116. package/src/adapters/godot/scene-export.yaml +39 -0
  117. package/src/adapters/hermes/sessions-search.yaml +61 -0
  118. package/src/adapters/hermes/skills-list.yaml +30 -0
  119. package/src/adapters/hermes/skills-read.yaml +46 -0
  120. package/src/adapters/homebrew/info.yaml +15 -0
  121. package/src/adapters/huggingface-papers/daily.yaml +21 -0
  122. package/src/adapters/infoq/articles.yaml +29 -0
  123. package/src/adapters/ip-info/lookup.yaml +15 -0
  124. package/src/adapters/itch-io/popular.yaml +22 -0
  125. package/src/adapters/ithome/news.yaml +21 -0
  126. package/src/adapters/mastodon/search.yaml +29 -0
  127. package/src/adapters/mastodon/trending.yaml +27 -0
  128. package/src/adapters/meituan/search.yaml +30 -0
  129. package/src/adapters/minimax/chat.yaml +33 -0
  130. package/src/adapters/minimax/models.yaml +18 -0
  131. package/src/adapters/minimax/tts.yaml +33 -0
  132. package/src/adapters/motion-studio/component-get.yaml +35 -0
  133. package/src/adapters/netease-music/hot.yaml +24 -0
  134. package/src/adapters/netease-music/search.yaml +29 -0
  135. package/src/adapters/npm-trends/compare.yaml +19 -0
  136. package/src/adapters/nytimes/top.yaml +26 -0
  137. package/src/adapters/openharness/memory-read.yaml +51 -0
  138. package/src/adapters/openharness/skills-list.yaml +28 -0
  139. package/src/adapters/openrouter/models.yaml +22 -0
  140. package/src/adapters/pexels/search.yaml +28 -0
  141. package/src/adapters/pinduoduo/hot.yaml +20 -0
  142. package/src/adapters/pypi/info.yaml +16 -0
  143. package/src/adapters/qweather/now.yaml +16 -0
  144. package/src/adapters/renderdoc/capture-list.yaml +42 -0
  145. package/src/adapters/renderdoc/frame-export.yaml +32 -0
  146. package/src/adapters/replicate/search.yaml +25 -0
  147. package/src/adapters/replicate/trending.yaml +22 -0
  148. package/src/adapters/sspai/hot.yaml +21 -0
  149. package/src/adapters/sspai/latest.yaml +22 -0
  150. package/src/adapters/stagehand/wrap-observe.yaml +42 -0
  151. package/src/adapters/techcrunch/latest.yaml +22 -0
  152. package/src/adapters/theverge/latest.yaml +21 -0
  153. package/src/adapters/twitch/top.yaml +26 -0
  154. package/src/adapters/unsplash/search.yaml +28 -0
  155. package/src/adapters/ycombinator/launches.yaml +20 -0
@@ -1,21 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MCP (Model Context Protocol) stdio server for Uni-CLI.
3
+ * MCP (Model Context Protocol) server for Uni-CLI.
4
4
  *
5
- * Lazy tool registration strategy:
6
- * - At startup, only two tools are registered: list_adapters + run_command
7
- * - run_command takes site + command as params, resolves the adapter dynamically
8
- * - This avoids registering 600+ tools upfront, keeping MCP handshake fast
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
- * Protocol: JSON-RPC 2.0 over stdio (newline-delimited JSON)
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
- // ── Tool Definitions ────────────────────────────────────────────────────────
18
- function buildCoreTools() {
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 handleRunCommand(params) {
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}/${command}.yaml`,
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
- const tools = buildCoreTools();
208
- function handleRequest(req) {
209
- const id = req.id ?? null;
210
- switch (req.method) {
211
- case "initialize":
212
- return {
213
- jsonrpc: "2.0",
214
- id,
215
- result: {
216
- protocolVersion: PROTOCOL_VERSION,
217
- capabilities: {
218
- tools: {},
219
- },
220
- serverInfo: {
221
- name: "unicli",
222
- version: VERSION,
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
- case "notifications/initialized":
227
- // Client acknowledgement — no response needed for notifications
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
- error: { code: -32602, message: "Missing tool name" },
446
+ result: { tools },
242
447
  };
243
- }
244
- const toolArgs = params.arguments ?? {};
245
- switch (params.name) {
246
- case "list_adapters": {
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
- result,
255
- }));
256
- default:
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: `Unknown tool: ${params.name}. Available tools: ${tools.map((t) => t.name).join(", ")}`,
535
+ message: `Method not found: ${req.method}`,
263
536
  },
264
537
  };
265
- }
538
+ }
539
+ return null;
266
540
  }
267
- case "ping":
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 main() {
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 handleRequest(req);
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
- // Log to stderr so it doesn't interfere with JSON-RPC on stdout
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
- process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (2 tools registered)\n`);
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`);