@zenalexa/unicli 0.210.0 → 0.211.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/AGENTS.md +21 -7
  2. package/README.md +680 -83
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +5 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/agents.d.ts +14 -3
  7. package/dist/commands/agents.d.ts.map +1 -1
  8. package/dist/commands/agents.js +369 -140
  9. package/dist/commands/agents.js.map +1 -1
  10. package/dist/commands/mcp.d.ts +3 -4
  11. package/dist/commands/mcp.d.ts.map +1 -1
  12. package/dist/commands/mcp.js +47 -62
  13. package/dist/commands/mcp.js.map +1 -1
  14. package/dist/commands/schema.d.ts +12 -0
  15. package/dist/commands/schema.d.ts.map +1 -0
  16. package/dist/commands/schema.js +72 -0
  17. package/dist/commands/schema.js.map +1 -0
  18. package/dist/commands/search.d.ts +12 -0
  19. package/dist/commands/search.d.ts.map +1 -0
  20. package/dist/commands/search.js +47 -0
  21. package/dist/commands/search.js.map +1 -0
  22. package/dist/discovery/aliases.d.ts +31 -0
  23. package/dist/discovery/aliases.d.ts.map +1 -0
  24. package/dist/discovery/aliases.js +477 -0
  25. package/dist/discovery/aliases.js.map +1 -0
  26. package/dist/discovery/loader.d.ts.map +1 -1
  27. package/dist/discovery/loader.js +25 -0
  28. package/dist/discovery/loader.js.map +1 -1
  29. package/dist/discovery/search.d.ts +73 -0
  30. package/dist/discovery/search.d.ts.map +1 -0
  31. package/dist/discovery/search.js +355 -0
  32. package/dist/discovery/search.js.map +1 -0
  33. package/dist/manifest-compact.txt +15 -0
  34. package/dist/manifest-search.json +1 -0
  35. package/dist/manifest.json +433 -244
  36. package/dist/mcp/oauth.d.ts +33 -0
  37. package/dist/mcp/oauth.d.ts.map +1 -0
  38. package/dist/mcp/oauth.js +220 -0
  39. package/dist/mcp/oauth.js.map +1 -0
  40. package/dist/mcp/schema.d.ts +65 -0
  41. package/dist/mcp/schema.d.ts.map +1 -0
  42. package/dist/mcp/schema.js +136 -0
  43. package/dist/mcp/schema.js.map +1 -0
  44. package/dist/mcp/server.d.ts +23 -10
  45. package/dist/mcp/server.d.ts.map +1 -1
  46. package/dist/mcp/server.js +350 -182
  47. package/dist/mcp/server.js.map +1 -1
  48. package/dist/mcp/sse-transport.d.ts +34 -0
  49. package/dist/mcp/sse-transport.d.ts.map +1 -0
  50. package/dist/mcp/sse-transport.js +182 -0
  51. package/dist/mcp/sse-transport.js.map +1 -0
  52. package/dist/mcp/streamable-http.d.ts +64 -0
  53. package/dist/mcp/streamable-http.d.ts.map +1 -0
  54. package/dist/mcp/streamable-http.js +312 -0
  55. package/dist/mcp/streamable-http.js.map +1 -0
  56. package/dist/permissions/sensitive-paths.js +2 -2
  57. package/dist/permissions/sensitive-paths.js.map +1 -1
  58. package/package.json +7 -7
  59. package/src/adapters/1688/_site.json +9 -0
  60. package/src/adapters/barchart/_site.json +10 -0
  61. package/src/adapters/jd/_site.json +9 -0
  62. package/src/adapters/linkedin/_site.json +10 -0
  63. package/src/adapters/macos/finder-copy.yaml +40 -0
  64. package/src/adapters/macos/finder-move.yaml +40 -0
  65. package/src/adapters/macos/finder-new-folder.yaml +36 -0
  66. package/src/adapters/macos/safari-history.yaml +23 -0
  67. package/src/adapters/macos/safari-url.yaml +22 -0
  68. package/src/adapters/macos/screen-recording.yaml +32 -0
  69. package/src/adapters/macos/wallpaper.yaml +33 -0
  70. package/src/adapters/reuters/_site.json +9 -0
  71. package/src/adapters/sinablog/_site.json +9 -0
  72. package/src/adapters/smzdm/_site.json +9 -0
  73. package/src/adapters/weixin/_site.json +9 -0
  74. package/src/adapters/1688/manifest.yaml +0 -7
  75. package/src/adapters/barchart/manifest.yaml +0 -8
  76. package/src/adapters/jd/manifest.yaml +0 -7
  77. package/src/adapters/linkedin/manifest.yaml +0 -8
  78. package/src/adapters/reuters/manifest.yaml +0 -7
  79. package/src/adapters/sinablog/manifest.yaml +0 -7
  80. package/src/adapters/smzdm/manifest.yaml +0 -7
  81. package/src/adapters/weixin/manifest.yaml +0 -7
@@ -3,20 +3,19 @@
3
3
  * MCP (Model Context Protocol) server for Uni-CLI.
4
4
  *
5
5
  * Two registration modes:
6
- * 1. **Expanded (default)** — one tool per adapter command
6
+ * 1. **Smart default** — 3 tools: `unicli_run`, `unicli_list`,
7
+ * `unicli_discover`. Keeps the MCP handshake under 200 tokens.
8
+ * 2. **Expanded (`--expanded`)** — one tool per adapter command
7
9
  * (`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.
10
+ * `columns`. MCP clients see the full Uni-CLI surface area.
14
11
  *
15
- * Two transports:
12
+ * Three transports:
16
13
  * - **stdio (default)** — newline-delimited JSON over stdin/stdout
17
14
  * - **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.
15
+ * single JSON-RPC envelope and returns a single JSON response.
16
+ * - **sse (`--transport sse [--port 19826]`)** Streamable HTTP with
17
+ * Server-Sent Events. GET /mcp/sse opens the event stream, POST
18
+ * /mcp/message?sessionId=xxx delivers JSON-RPC requests.
20
19
  *
21
20
  * Auth pass-through is automatic: every adapter the CLI loads (including
22
21
  * cookie-based ones) is exposed by name; the runtime resolves cookies on
@@ -28,174 +27,143 @@ import { loadAllAdapters, loadTsAdapters } from "../discovery/loader.js";
28
27
  import { getAllAdapters, listCommands, resolveCommand } from "../registry.js";
29
28
  import { runPipeline } from "../engine/yaml-runner.js";
30
29
  import { VERSION } from "../constants.js";
31
- // ── Lazy-mode core tools (preserved for `--lazy` flag) ──────────────────────
32
- function buildLazyTools() {
30
+ // sse-transport.ts is deprecated (spec 2025-03-26). Kept for backwards compatibility.
31
+ // import { startSseServer } from "./sse-transport.js";
32
+ import { startStreamableHttp } from "./streamable-http.js";
33
+ import { handleOAuthRoute, createOAuthMiddleware } from "./oauth.js";
34
+ import { buildInputSchema, buildOutputSchema, buildToolName, truncateDescription, } from "./schema.js";
35
+ // ── Smart default tools (4 meta-tools — the default mode) ─────────────────
36
+ const MAX_RESULT_SIZE_CHARS = 10_000;
37
+ function buildDefaultTools() {
33
38
  return [
34
39
  {
35
- name: "list_adapters",
36
- description: "List all available Uni-CLI adapters and their commands. " +
37
- "Use this to discover what sites and commands are available before calling run_command.",
40
+ name: "unicli_run",
41
+ description: "Execute any Uni-CLI command. Returns JSON results.",
38
42
  inputSchema: {
39
43
  type: "object",
40
44
  properties: {
41
45
  site: {
42
46
  type: "string",
43
- description: "Filter by site name (optional, partial match)",
47
+ description: "Site name (e.g. hackernews, github, bilibili)",
44
48
  },
45
- type: {
49
+ command: {
46
50
  type: "string",
47
- description: "Filter by adapter type: web-api, desktop, browser, bridge, service",
48
- enum: ["web-api", "desktop", "browser", "bridge", "service"],
51
+ description: "Command to run (e.g. top, search, hot)",
52
+ },
53
+ args: {
54
+ type: "object",
55
+ description: 'Key-value arguments (e.g. {"query": "ai", "limit": 10})',
56
+ additionalProperties: true,
49
57
  },
50
58
  },
59
+ required: ["site", "command"],
60
+ },
61
+ _meta: {
62
+ "anthropic/searchHint": "Execute CLI commands on 200+ websites and desktop apps. Run adapters by site and command name.",
63
+ },
64
+ annotations: {
65
+ readOnlyHint: false,
66
+ destructiveHint: false,
67
+ idempotentHint: false,
68
+ openWorldHint: true,
51
69
  },
52
70
  },
53
71
  {
54
- name: "run_command",
55
- description: "Execute a Uni-CLI adapter command. Equivalent to running `unicli <site> <command>` on the CLI. " +
56
- "Returns JSON results. Use list_adapters first to discover available commands.",
72
+ name: "unicli_list",
73
+ description: "List available commands. Filter by site or adapter type.",
57
74
  inputSchema: {
58
75
  type: "object",
59
76
  properties: {
60
77
  site: {
61
78
  type: "string",
62
- description: "The adapter site name (e.g. hackernews, github, bilibili)",
79
+ description: "Filter by site name (partial match)",
63
80
  },
64
- command: {
81
+ type: {
65
82
  type: "string",
66
- description: "The command to run (e.g. top, search, hot)",
67
- },
68
- args: {
69
- type: "object",
70
- description: 'Command arguments as key-value pairs (e.g. {"query": "ai", "limit": 10})',
71
- additionalProperties: true,
83
+ description: "Filter by adapter type",
84
+ enum: ["web-api", "desktop", "browser", "bridge", "service"],
72
85
  },
73
86
  },
74
- required: ["site", "command"],
87
+ },
88
+ _meta: {
89
+ "anthropic/searchHint": "Browse available Uni-CLI sites and commands. Filter by site name or adapter type.",
90
+ "anthropic/alwaysLoad": true,
91
+ },
92
+ annotations: {
93
+ readOnlyHint: true,
94
+ idempotentHint: true,
95
+ openWorldHint: false,
75
96
  },
76
97
  },
77
- ];
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,
98
+ {
99
+ name: "unicli_search",
100
+ description: "Search 200+ sites and 956 commands by intent. Bilingual (EN/ZH). Returns top matches with usage examples.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ query: {
105
+ type: "string",
106
+ description: "Natural language intent (e.g. 'download video', '推特热门', 'stock price')",
107
+ },
108
+ limit: {
109
+ type: "integer",
110
+ description: "Max results (default 5)",
111
+ default: 5,
112
+ },
113
+ },
114
+ required: ["query"],
115
+ },
116
+ _meta: {
117
+ "anthropic/searchHint": "Find CLI commands by intent. Semantic search across websites, desktop apps, macOS. Bilingual Chinese/English.",
118
+ "anthropic/alwaysLoad": true,
119
+ },
120
+ annotations: {
121
+ readOnlyHint: true,
122
+ idempotentHint: true,
123
+ openWorldHint: false,
124
+ },
106
125
  },
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
- : {}),
126
+ {
127
+ name: "unicli_explore",
128
+ description: "Auto-discover API endpoints for any URL. Navigates the page, captures network requests, generates YAML adapters.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ url: { type: "string", description: "Website URL to explore" },
133
+ goal: {
134
+ type: "string",
135
+ description: "Capability to find (e.g. 'search', 'hot', 'feed')",
136
+ },
166
137
  },
138
+ required: ["url"],
139
+ },
140
+ _meta: {
141
+ "anthropic/searchHint": "Auto-discover API endpoints for any website URL. Generate YAML adapters for new sites.",
142
+ },
143
+ annotations: {
144
+ readOnlyHint: false,
145
+ destructiveHint: false,
146
+ idempotentHint: false,
147
+ openWorldHint: true,
167
148
  },
168
149
  },
169
- };
150
+ ];
170
151
  }
152
+ const expandedRegistry = new Map();
171
153
  /**
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.
154
+ * Build the expanded tool set: 4 default meta-tools + one full tool per
155
+ * adapter command. Clients see the complete Uni-CLI surface area.
175
156
  *
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.
157
+ * Token cost: ~160K for 956 commands. Use only when the client can handle it.
181
158
  */
182
- function buildToolName(site, command) {
183
- return `unicli_${site}_${command}`.replace(/[^a-zA-Z0-9_]/g, "_");
184
- }
185
- const expandedRegistry = new Map();
186
159
  function buildExpandedTools() {
187
160
  const tools = [];
188
- // Always include list_adapters for discovery + as a smoke test.
189
- tools.push(buildLazyTools()[0]);
161
+ tools.push(...buildDefaultTools());
190
162
  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();
163
+ const seen = new Set(DEFAULT_TOOL_NAMES);
196
164
  for (const adapter of getAllAdapters()) {
197
165
  for (const [cmdName, cmd] of Object.entries(adapter.commands)) {
198
- const description = cmd.description?.trim() ||
166
+ const rawDesc = cmd.description?.trim() ||
199
167
  adapter.description?.trim() ||
200
168
  `${cmdName} for ${adapter.name}`;
201
169
  const toolName = buildToolName(adapter.name, cmdName);
@@ -207,30 +175,82 @@ function buildExpandedTools() {
207
175
  expandedRegistry.set(toolName, { adapter, cmdName, cmd });
208
176
  tools.push({
209
177
  name: toolName,
210
- description: `[${adapter.name}] ${description}`,
178
+ description: truncateDescription(`[${adapter.name}] ${rawDesc}`),
211
179
  inputSchema: buildInputSchema(cmd),
212
180
  outputSchema: buildOutputSchema(cmd),
181
+ _meta: {
182
+ "anthropic/searchHint": `${adapter.name}: ${rawDesc}`,
183
+ },
184
+ annotations: {
185
+ readOnlyHint: true,
186
+ idempotentHint: true,
187
+ openWorldHint: true,
188
+ },
213
189
  });
214
190
  }
215
191
  }
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')",
192
+ return tools;
193
+ }
194
+ /**
195
+ * Build deferred tool set: 4 default meta-tools with full schemas, plus
196
+ * lightweight stubs for all adapter commands (name + searchHint only,
197
+ * minimal inputSchema). Clients like Claude Code's ToolSearch can discover
198
+ * tools by searchHint and then call them — the handler resolves the full
199
+ * command at call time via the expandedRegistry.
200
+ *
201
+ * Token cost: ~8K (vs ~160K for expanded). 95% reduction.
202
+ */
203
+ function buildDeferredTools() {
204
+ const tools = [];
205
+ tools.push(...buildDefaultTools());
206
+ expandedRegistry.clear();
207
+ const seen = new Set(DEFAULT_TOOL_NAMES);
208
+ for (const adapter of getAllAdapters()) {
209
+ for (const [cmdName, cmd] of Object.entries(adapter.commands)) {
210
+ const rawDesc = cmd.description?.trim() ||
211
+ adapter.description?.trim() ||
212
+ `${cmdName} for ${adapter.name}`;
213
+ const toolName = buildToolName(adapter.name, cmdName);
214
+ if (seen.has(toolName))
215
+ continue;
216
+ seen.add(toolName);
217
+ // Register in the lookup table for runtime dispatch
218
+ expandedRegistry.set(toolName, { adapter, cmdName, cmd });
219
+ // Lightweight stub: name + searchHint + minimal schema.
220
+ // Full inputSchema is resolved at call time via expandedRegistry.
221
+ tools.push({
222
+ name: toolName,
223
+ description: truncateDescription(`[${adapter.name}] ${rawDesc}`),
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ _args: {
228
+ type: "object",
229
+ description: "Command arguments (pass key-value pairs)",
230
+ additionalProperties: true,
231
+ },
232
+ },
227
233
  },
228
- },
229
- required: ["url"],
230
- },
231
- });
234
+ _meta: {
235
+ "anthropic/searchHint": `${adapter.name}: ${rawDesc}`,
236
+ },
237
+ annotations: {
238
+ readOnlyHint: true,
239
+ idempotentHint: true,
240
+ openWorldHint: true,
241
+ },
242
+ });
243
+ }
244
+ }
232
245
  return tools;
233
246
  }
247
+ const DEFAULT_TOOL_NAMES = new Set([
248
+ "unicli_run",
249
+ "unicli_list",
250
+ "unicli_search",
251
+ "unicli_explore",
252
+ "unicli_discover",
253
+ ]);
234
254
  // ── Tool Handlers ───────────────────────────────────────────────────────────
235
255
  function handleListAdapters(params) {
236
256
  let commands = listCommands();
@@ -331,6 +351,21 @@ async function runResolvedCommand(adapter, cmd, cmdName, args) {
331
351
  };
332
352
  }
333
353
  }
354
+ /**
355
+ * Annotate a tool result with `_meta.anthropic/maxResultSizeChars` when the
356
+ * serialized payload exceeds MAX_RESULT_SIZE_CHARS (10 KB). This tells
357
+ * Claude Code to accept large payloads without truncation.
358
+ */
359
+ export function annotateIfLarge(result) {
360
+ const totalChars = result.content.reduce((sum, c) => sum + c.text.length, 0);
361
+ if (totalChars > MAX_RESULT_SIZE_CHARS) {
362
+ return {
363
+ ...result,
364
+ _meta: { "anthropic/maxResultSizeChars": 100_000 },
365
+ };
366
+ }
367
+ return result;
368
+ }
334
369
  async function handleRunCommand(params) {
335
370
  const site = params.site;
336
371
  const command = params.command;
@@ -375,7 +410,7 @@ async function handleRunCommand(params) {
375
410
  /**
376
411
  * Expanded-tool dispatcher — parse `unicli_<site>_<command>` back to its
377
412
  * 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
413
+ * is not in expanded form, so the caller can fall through to default-tool
379
414
  * handling (list_adapters / run_command).
380
415
  */
381
416
  async function handleExpandedTool(toolName, args) {
@@ -394,21 +429,29 @@ async function handleExpandedTool(toolName, args) {
394
429
  return runResolvedCommand(entry.adapter, entry.cmd, entry.cmdName, args);
395
430
  }
396
431
  // ── MCP Protocol Handler ────────────────────────────────────────────────────
397
- const PROTOCOL_VERSION = "2024-11-05";
432
+ const PROTOCOL_VERSION = "2025-03-26";
398
433
  function parseArgs(argv) {
399
434
  const opts = {
400
- lazy: false,
435
+ expanded: false,
401
436
  transport: "stdio",
402
437
  port: 19826,
438
+ auth: false,
403
439
  };
404
440
  for (let i = 0; i < argv.length; i++) {
405
441
  const a = argv[i];
406
- if (a === "--lazy")
407
- opts.lazy = true;
442
+ if (a === "--expanded")
443
+ opts.expanded = true;
444
+ else if (a === "--auth")
445
+ opts.auth = true;
408
446
  else if (a === "--transport") {
409
447
  const v = argv[++i];
410
- if (v === "stdio" || v === "http")
448
+ if (v === "stdio" || v === "http" || v === "streamable") {
411
449
  opts.transport = v;
450
+ }
451
+ else if (v === "sse") {
452
+ // Deprecated alias — SSE replaced by Streamable HTTP in spec 2025-03-26
453
+ opts.transport = "streamable";
454
+ }
412
455
  }
413
456
  else if (a === "--port") {
414
457
  const v = parseInt(argv[++i], 10);
@@ -429,7 +472,7 @@ function buildHandler(tools) {
429
472
  result: {
430
473
  protocolVersion: PROTOCOL_VERSION,
431
474
  capabilities: {
432
- tools: {},
475
+ tools: { listChanged: false },
433
476
  },
434
477
  serverInfo: {
435
478
  name: "unicli",
@@ -438,6 +481,8 @@ function buildHandler(tools) {
438
481
  },
439
482
  };
440
483
  case "notifications/initialized":
484
+ // JSON-RPC notifications must not receive responses.
485
+ // Returning a sentinel that transports check before serializing.
441
486
  return null;
442
487
  case "tools/list":
443
488
  return {
@@ -456,17 +501,65 @@ function buildHandler(tools) {
456
501
  }
457
502
  const toolArgs = params.arguments ?? {};
458
503
  switch (params.name) {
504
+ // Support both old names (list_adapters, run_command) and new
505
+ // names (unicli_list, unicli_run) for backwards compatibility.
506
+ case "unicli_list":
459
507
  case "list_adapters": {
460
508
  const result = handleListAdapters(toolArgs);
461
- return { jsonrpc: "2.0", id, result };
509
+ return { jsonrpc: "2.0", id, result: annotateIfLarge(result) };
462
510
  }
511
+ case "unicli_run":
463
512
  case "run_command":
464
513
  return handleRunCommand(toolArgs).then((result) => ({
465
514
  jsonrpc: "2.0",
466
515
  id,
467
- result,
516
+ result: annotateIfLarge(result),
468
517
  }));
518
+ case "unicli_search": {
519
+ const searchQuery = toolArgs.query;
520
+ const searchLimit = toolArgs.limit || 5;
521
+ if (!searchQuery) {
522
+ return {
523
+ jsonrpc: "2.0",
524
+ id,
525
+ error: {
526
+ code: -32602,
527
+ message: "Missing required parameter: query",
528
+ },
529
+ };
530
+ }
531
+ return import("../discovery/search.js").then(({ search: searchFn }) => {
532
+ const results = searchFn(searchQuery, searchLimit);
533
+ return {
534
+ jsonrpc: "2.0",
535
+ id,
536
+ result: annotateIfLarge({
537
+ content: [
538
+ {
539
+ type: "text",
540
+ text: JSON.stringify({
541
+ query: searchQuery,
542
+ count: results.length,
543
+ results: results.map((r) => ({
544
+ command: `unicli ${r.site} ${r.command}`,
545
+ site: r.site,
546
+ name: r.command,
547
+ description: r.description,
548
+ score: r.score,
549
+ category: r.category,
550
+ usage: r.usage,
551
+ })),
552
+ }, null, 2),
553
+ },
554
+ ],
555
+ }),
556
+ };
557
+ });
558
+ }
559
+ case "unicli_explore":
469
560
  case "unicli_discover": {
561
+ // unicli_explore is the canonical name (v0.211.1+).
562
+ // unicli_discover kept as alias for backwards compatibility.
470
563
  const discoverUrl = toolArgs.url;
471
564
  const discoverGoal = toolArgs.goal;
472
565
  if (!discoverUrl) {
@@ -479,6 +572,17 @@ function buildHandler(tools) {
479
572
  },
480
573
  };
481
574
  }
575
+ if (!discoverUrl.startsWith("http://") &&
576
+ !discoverUrl.startsWith("https://")) {
577
+ return {
578
+ jsonrpc: "2.0",
579
+ id,
580
+ error: {
581
+ code: -32602,
582
+ message: "URL must start with http:// or https://",
583
+ },
584
+ };
585
+ }
482
586
  return import("node:child_process").then(({ execFile: ef }) => import("node:util").then(({ promisify: prom }) => {
483
587
  const execFileP = prom(ef);
484
588
  const discoverArgs = ["generate", discoverUrl, "--json"];
@@ -490,11 +594,13 @@ function buildHandler(tools) {
490
594
  }).then(({ stdout }) => ({
491
595
  jsonrpc: "2.0",
492
596
  id,
493
- result: { content: [{ type: "text", text: stdout }] },
597
+ result: annotateIfLarge({
598
+ content: [{ type: "text", text: stdout }],
599
+ }),
494
600
  }), (err) => ({
495
601
  jsonrpc: "2.0",
496
602
  id,
497
- result: {
603
+ result: annotateIfLarge({
498
604
  content: [
499
605
  {
500
606
  type: "text",
@@ -504,20 +610,20 @@ function buildHandler(tools) {
504
610
  },
505
611
  ],
506
612
  isError: true,
507
- },
613
+ }),
508
614
  }));
509
615
  }));
510
616
  }
511
617
  default:
512
618
  return handleExpandedTool(params.name, toolArgs).then((result) => {
513
619
  if (result)
514
- return { jsonrpc: "2.0", id, result };
620
+ return { jsonrpc: "2.0", id, result: annotateIfLarge(result) };
515
621
  return {
516
622
  jsonrpc: "2.0",
517
623
  id,
518
624
  error: {
519
- code: -32601,
520
- message: `Unknown tool: ${params.name}. Use list_adapters to see all available commands.`,
625
+ code: -32602,
626
+ message: `Unknown tool: ${params.name}. Use unicli_list to see available commands.`,
521
627
  },
522
628
  };
523
629
  });
@@ -596,15 +702,29 @@ async function startStdio(handler) {
596
702
  * to MCP only need request/response, and starting with the simpler shape
597
703
  * means zero new dependencies and a tiny attack surface.
598
704
  */
599
- async function startHttp(handler, port, toolCount) {
705
+ async function startHttp(handler, port, authEnabled = false) {
706
+ const oauthMiddleware = authEnabled ? createOAuthMiddleware() : null;
600
707
  const server = createServer((req, res) => {
601
- if (req.method === "GET" && (req.url === "/" || req.url === "/mcp")) {
708
+ // OAuth routes (authorize + token) always public
709
+ if (authEnabled && handleOAuthRoute(req, res))
710
+ return;
711
+ // Health endpoint — always public
712
+ if (req.method === "GET" &&
713
+ (req.url === "/" || req.url === "/mcp" || req.url === "/health")) {
602
714
  res.writeHead(200, { "Content-Type": "application/json" });
715
+ const adapterCount = getAllAdapters().length;
716
+ const commandCount = listCommands().length;
717
+ // Compute actual expanded tool count: 3 default tools + all adapter commands.
718
+ let expandedCount = 3; // default tools
719
+ for (const adapter of getAllAdapters()) {
720
+ expandedCount += Object.keys(adapter.commands).length;
721
+ }
603
722
  res.end(JSON.stringify({
604
- server: "unicli",
723
+ status: "ok",
724
+ adapters: adapterCount,
725
+ commands: commandCount,
726
+ tools: { default: 3, expanded: expandedCount },
605
727
  version: VERSION,
606
- tools: toolCount,
607
- protocol: PROTOCOL_VERSION,
608
728
  }));
609
729
  return;
610
730
  }
@@ -613,9 +733,31 @@ async function startHttp(handler, port, toolCount) {
613
733
  res.end(JSON.stringify({ error: "POST /mcp" }));
614
734
  return;
615
735
  }
736
+ // OAuth middleware — block unauthenticated requests when --auth is set
737
+ if (oauthMiddleware?.(req, res))
738
+ return;
739
+ const MAX_BODY = 1_048_576; // 1 MB
616
740
  const chunks = [];
617
- req.on("data", (chunk) => chunks.push(chunk));
741
+ let bodySize = 0;
742
+ let aborted = false;
743
+ req.on("data", (chunk) => {
744
+ bodySize += chunk.length;
745
+ if (bodySize > MAX_BODY) {
746
+ aborted = true;
747
+ res.writeHead(413, { "Content-Type": "application/json" });
748
+ res.end(JSON.stringify({
749
+ jsonrpc: "2.0",
750
+ id: null,
751
+ error: { code: -32600, message: "Request too large" },
752
+ }));
753
+ req.destroy();
754
+ return;
755
+ }
756
+ chunks.push(chunk);
757
+ });
618
758
  req.on("end", async () => {
759
+ if (aborted)
760
+ return;
619
761
  const body = Buffer.concat(chunks).toString("utf-8");
620
762
  let parsed;
621
763
  try {
@@ -632,8 +774,14 @@ async function startHttp(handler, port, toolCount) {
632
774
  }
633
775
  try {
634
776
  const response = await handler(parsed);
777
+ if (!response) {
778
+ // JSON-RPC notification — no response expected
779
+ res.writeHead(204);
780
+ res.end();
781
+ return;
782
+ }
635
783
  res.writeHead(200, { "Content-Type": "application/json" });
636
- res.end(JSON.stringify(response ?? null));
784
+ res.end(JSON.stringify(response));
637
785
  }
638
786
  catch (err) {
639
787
  const message = err instanceof Error ? err.message : String(err);
@@ -661,18 +809,38 @@ async function main() {
661
809
  // Load adapters (same as CLI)
662
810
  loadAllAdapters();
663
811
  await loadTsAdapters();
664
- const tools = opts.lazy ? buildLazyTools() : buildExpandedTools();
812
+ // Three modes:
813
+ // default → 4 meta-tools (~200 tokens)
814
+ // expanded → 4 meta-tools + 956 full tool schemas (~160K tokens)
815
+ // deferred → 4 meta-tools + 956 lightweight stubs (~8K tokens)
816
+ const mode = opts.expanded ? "expanded" : "default";
817
+ const tools = opts.expanded ? buildExpandedTools() : buildDefaultTools();
818
+ // Deferred mode is auto-activated for Streamable HTTP transport (remote
819
+ // clients benefit most from searchHint-based discovery).
820
+ // For explicit control, the expanded flag takes precedence.
821
+ if (opts.transport === "streamable" && !opts.expanded) {
822
+ const deferredTools = buildDeferredTools();
823
+ tools.length = 0;
824
+ tools.push(...deferredTools);
825
+ }
665
826
  const handler = buildHandler(tools);
666
827
  const adapterCount = getAllAdapters().length;
667
828
  const commandCount = listCommands().length;
668
829
  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`);
830
+ await startHttp(handler, opts.port, opts.auth);
831
+ const authLabel = opts.auth ? ", OAuth enabled" : "";
832
+ process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools registered, mode=${mode}${authLabel})\n`);
833
+ return;
834
+ }
835
+ if (opts.transport === "streamable") {
836
+ await startStreamableHttp(opts.port, handler, { auth: opts.auth });
837
+ const authLabel = opts.auth ? ", OAuth enabled" : "";
838
+ process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools, mode=${mode}, transport=streamable${authLabel})\n`);
671
839
  return;
672
840
  }
673
841
  // stdio (default)
674
842
  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`);
843
+ process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools registered, mode=${mode})\n`);
676
844
  }
677
845
  main().catch((err) => {
678
846
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);