@zenalexa/unicli 0.213.1 → 0.213.3
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 +1 -1
- package/README.md +4 -4
- package/README.zh-CN.md +3 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/describe.d.ts +26 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +230 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/dev.js +1 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/dispatch.d.ts +14 -3
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/commands/dispatch.js +141 -81
- package/dist/commands/dispatch.js.map +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/commands/skills.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/discovery/loader.d.ts +8 -0
- package/dist/discovery/loader.d.ts.map +1 -1
- package/dist/discovery/loader.js +22 -1
- package/dist/discovery/loader.js.map +1 -1
- package/dist/engine/args.d.ts +83 -0
- package/dist/engine/args.d.ts.map +1 -0
- package/dist/engine/args.js +260 -0
- package/dist/engine/args.js.map +1 -0
- package/dist/engine/executor.d.ts +20 -1
- package/dist/engine/executor.d.ts.map +1 -1
- package/dist/engine/executor.js +11 -2
- package/dist/engine/executor.js.map +1 -1
- package/dist/engine/harden.d.ts +48 -0
- package/dist/engine/harden.d.ts.map +1 -0
- package/dist/engine/harden.js +327 -0
- package/dist/engine/harden.js.map +1 -0
- package/dist/engine/invoke.d.ts +16 -0
- package/dist/engine/invoke.d.ts.map +1 -0
- package/dist/engine/invoke.js +15 -0
- package/dist/engine/invoke.js.map +1 -0
- package/dist/engine/kernel/compile.d.ts +30 -0
- package/dist/engine/kernel/compile.d.ts.map +1 -0
- package/dist/engine/kernel/compile.js +167 -0
- package/dist/engine/kernel/compile.js.map +1 -0
- package/dist/engine/kernel/execute.d.ts +35 -0
- package/dist/engine/kernel/execute.d.ts.map +1 -0
- package/dist/engine/kernel/execute.js +221 -0
- package/dist/engine/kernel/execute.js.map +1 -0
- package/dist/engine/kernel/types.d.ts +49 -0
- package/dist/engine/kernel/types.d.ts.map +1 -0
- package/dist/engine/kernel/types.js +9 -0
- package/dist/engine/kernel/types.js.map +1 -0
- package/dist/engine/kernel/ulid.d.ts +21 -0
- package/dist/engine/kernel/ulid.d.ts.map +1 -0
- package/dist/engine/kernel/ulid.js +76 -0
- package/dist/engine/kernel/ulid.js.map +1 -0
- package/dist/engine/template.d.ts.map +1 -1
- package/dist/engine/template.js +11 -0
- package/dist/engine/template.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/mcp/dispatch.d.ts +46 -0
- package/dist/mcp/dispatch.d.ts.map +1 -0
- package/dist/mcp/dispatch.js +109 -0
- package/dist/mcp/dispatch.js.map +1 -0
- package/dist/mcp/handler.d.ts +30 -0
- package/dist/mcp/handler.d.ts.map +1 -0
- package/dist/mcp/handler.js +293 -0
- package/dist/mcp/handler.js.map +1 -0
- package/dist/mcp/http-transport.d.ts +16 -0
- package/dist/mcp/http-transport.d.ts.map +1 -0
- package/dist/mcp/http-transport.js +124 -0
- package/dist/mcp/http-transport.js.map +1 -0
- package/dist/mcp/server.d.ts +17 -33
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +31 -742
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/sse-transport.js.map +1 -1
- package/dist/mcp/streamable-http/handle-post.d.ts +23 -0
- package/dist/mcp/streamable-http/handle-post.d.ts.map +1 -0
- package/dist/mcp/streamable-http/handle-post.js +357 -0
- package/dist/mcp/streamable-http/handle-post.js.map +1 -0
- package/dist/mcp/streamable-http/index.d.ts +45 -0
- package/dist/mcp/streamable-http/index.d.ts.map +1 -0
- package/dist/mcp/streamable-http/index.js +150 -0
- package/dist/mcp/streamable-http/index.js.map +1 -0
- package/dist/mcp/streamable-http/session.d.ts +85 -0
- package/dist/mcp/streamable-http/session.d.ts.map +1 -0
- package/dist/mcp/streamable-http/session.js +104 -0
- package/dist/mcp/streamable-http/session.js.map +1 -0
- package/dist/mcp/streamable-http.d.ts +7 -85
- package/dist/mcp/streamable-http.d.ts.map +1 -1
- package/dist/mcp/streamable-http.js +6 -568
- package/dist/mcp/streamable-http.js.map +1 -1
- package/dist/mcp/tools.d.ts +57 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +237 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/output/envelope.d.ts +29 -0
- package/dist/output/envelope.d.ts.map +1 -1
- package/dist/output/envelope.js +6 -0
- package/dist/output/envelope.js.map +1 -1
- package/dist/output/next-actions.d.ts +18 -0
- package/dist/output/next-actions.d.ts.map +1 -0
- package/dist/output/next-actions.js +64 -0
- package/dist/output/next-actions.js.map +1 -0
- package/dist/output/projection.d.ts +93 -0
- package/dist/output/projection.d.ts.map +1 -0
- package/dist/output/projection.js +255 -0
- package/dist/output/projection.js.map +1 -0
- package/dist/protocol/acp-helpers.d.ts +32 -0
- package/dist/protocol/acp-helpers.d.ts.map +1 -0
- package/dist/protocol/acp-helpers.js +175 -0
- package/dist/protocol/acp-helpers.js.map +1 -0
- package/dist/protocol/acp.d.ts +2 -27
- package/dist/protocol/acp.d.ts.map +1 -1
- package/dist/protocol/acp.js +7 -170
- package/dist/protocol/acp.js.map +1 -1
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -2
- package/src/adapters/1688/item.yaml +1 -0
- package/src/adapters/1688/store.yaml +1 -0
- package/src/adapters/36kr/article.yaml +1 -0
- package/src/adapters/amazon/bestsellers.yaml +2 -0
- package/src/adapters/amazon/movers-shakers.yaml +2 -0
- package/src/adapters/amazon/new-releases.yaml +2 -0
- package/src/adapters/amazon/rankings.yaml +1 -0
- package/src/adapters/apple-podcasts/episodes.yaml +1 -0
- package/src/adapters/arxiv/paper.yaml +1 -0
- package/src/adapters/audacity/convert.yaml +1 -0
- package/src/adapters/audacity/effects.yaml +1 -0
- package/src/adapters/audacity/info.yaml +1 -0
- package/src/adapters/audacity/mix.yaml +1 -0
- package/src/adapters/audacity/normalize.yaml +1 -0
- package/src/adapters/audacity/spectrogram.yaml +1 -0
- package/src/adapters/audacity/split-channels.yaml +1 -0
- package/src/adapters/audacity/trim.yaml +1 -0
- package/src/adapters/bilibili/favorites.yaml +1 -0
- package/src/adapters/blender/animation.yaml +2 -0
- package/src/adapters/blender/camera.yaml +1 -0
- package/src/adapters/blender/convert.yaml +1 -0
- package/src/adapters/blender/export.yaml +1 -0
- package/src/adapters/blender/import.yaml +1 -0
- package/src/adapters/blender/info.yaml +1 -0
- package/src/adapters/blender/lighting.yaml +1 -0
- package/src/adapters/blender/materials.yaml +1 -0
- package/src/adapters/blender/objects.yaml +1 -0
- package/src/adapters/blender/render.yaml +2 -0
- package/src/adapters/blender/scene.yaml +1 -0
- package/src/adapters/blender/screenshot.yaml +2 -0
- package/src/adapters/blender/script.yaml +1 -0
- package/src/adapters/boss/detail.yaml +2 -0
- package/src/adapters/cloudcompare/info.yaml +1 -0
- package/src/adapters/coupang/add-to-cart.yaml +1 -0
- package/src/adapters/ctrip/search.yaml +1 -0
- package/src/adapters/douban/download.yaml +1 -0
- package/src/adapters/douban/photos.yaml +1 -0
- package/src/adapters/douban/subject.yaml +2 -0
- package/src/adapters/doubao-web/detail.yaml +1 -0
- package/src/adapters/doubao-web/meeting-summary.yaml +1 -0
- package/src/adapters/doubao-web/meeting-transcript.yaml +1 -0
- package/src/adapters/drawio/export.yaml +2 -0
- package/src/adapters/facebook/join-group.yaml +2 -0
- package/src/adapters/ffmpeg/compress.yaml +2 -0
- package/src/adapters/ffmpeg/concat.yaml +1 -0
- package/src/adapters/ffmpeg/convert.yaml +1 -0
- package/src/adapters/ffmpeg/extract-audio.yaml +2 -0
- package/src/adapters/ffmpeg/gif.yaml +2 -0
- package/src/adapters/ffmpeg/normalize.yaml +2 -0
- package/src/adapters/ffmpeg/probe.yaml +1 -0
- package/src/adapters/ffmpeg/resize.yaml +2 -0
- package/src/adapters/ffmpeg/subtitles.yaml +2 -0
- package/src/adapters/ffmpeg/thumbnail.yaml +2 -0
- package/src/adapters/ffmpeg/trim.yaml +2 -0
- package/src/adapters/freecad/assembly.yaml +1 -0
- package/src/adapters/freecad/bom.yaml +1 -0
- package/src/adapters/freecad/boolean.yaml +1 -0
- package/src/adapters/freecad/check.yaml +1 -0
- package/src/adapters/freecad/convert.yaml +1 -0
- package/src/adapters/freecad/export-stl.yaml +1 -0
- package/src/adapters/freecad/import.yaml +1 -0
- package/src/adapters/freecad/info.yaml +1 -0
- package/src/adapters/freecad/macro.yaml +1 -0
- package/src/adapters/freecad/measure.yaml +1 -0
- package/src/adapters/freecad/mesh.yaml +1 -0
- package/src/adapters/freecad/properties.yaml +1 -0
- package/src/adapters/freecad/render.yaml +1 -0
- package/src/adapters/freecad/section.yaml +1 -0
- package/src/adapters/freecad/sketch.yaml +1 -0
- package/src/adapters/gimp/adjust.yaml +1 -0
- package/src/adapters/gimp/batch.yaml +1 -0
- package/src/adapters/gimp/convert.yaml +1 -0
- package/src/adapters/gimp/crop.yaml +1 -0
- package/src/adapters/gimp/filter.yaml +1 -0
- package/src/adapters/gimp/flip.yaml +1 -0
- package/src/adapters/gimp/info.yaml +1 -0
- package/src/adapters/gimp/layers.yaml +1 -0
- package/src/adapters/gimp/merge-layers.yaml +1 -0
- package/src/adapters/gimp/resize.yaml +1 -0
- package/src/adapters/gimp/rotate.yaml +1 -0
- package/src/adapters/gimp/text.yaml +1 -0
- package/src/adapters/godot/scene-export.yaml +1 -0
- package/src/adapters/hackernews/comments.yaml +1 -0
- package/src/adapters/hackernews/item.yaml +1 -0
- package/src/adapters/hf/top.yaml +1 -0
- package/src/adapters/imagemagick/composite.yaml +1 -0
- package/src/adapters/imagemagick/convert.yaml +1 -0
- package/src/adapters/imagemagick/identify.yaml +1 -0
- package/src/adapters/imagemagick/montage.yaml +1 -0
- package/src/adapters/imagemagick/resize.yaml +1 -0
- package/src/adapters/imdb/person.yaml +1 -0
- package/src/adapters/imdb/reviews.yaml +1 -0
- package/src/adapters/imdb/title.yaml +1 -0
- package/src/adapters/inkscape/convert.yaml +2 -0
- package/src/adapters/inkscape/export.yaml +2 -0
- package/src/adapters/inkscape/optimize.yaml +2 -0
- package/src/adapters/instagram/stories.yaml +1 -0
- package/src/adapters/jd/item.yaml +1 -0
- package/src/adapters/jike/post.yaml +2 -0
- package/src/adapters/jike/topic.yaml +2 -0
- package/src/adapters/kdenlive/info.yaml +1 -0
- package/src/adapters/kdenlive/render.yaml +1 -0
- package/src/adapters/krita/batch.yaml +1 -0
- package/src/adapters/krita/convert.yaml +1 -0
- package/src/adapters/krita/export.yaml +1 -0
- package/src/adapters/krita/info.yaml +1 -0
- package/src/adapters/libreoffice/convert.yaml +1 -0
- package/src/adapters/libreoffice/print.yaml +1 -0
- package/src/adapters/linear/issue-update.yaml +1 -0
- package/src/adapters/linux-do/category.yaml +1 -0
- package/src/adapters/linux-do/topic.yaml +1 -0
- package/src/adapters/macos/calendar-create.yaml +1 -0
- package/src/adapters/macos/finder-copy.yaml +1 -0
- package/src/adapters/macos/finder-move.yaml +1 -0
- package/src/adapters/macos/finder-new-folder.yaml +1 -0
- package/src/adapters/macos/finder-tags.yaml +1 -0
- package/src/adapters/macos/open.yaml +2 -0
- package/src/adapters/macos/screen-recording.yaml +1 -0
- package/src/adapters/macos/screenshot.yaml +1 -0
- package/src/adapters/macos/spotlight.yaml +1 -0
- package/src/adapters/macos/wallpaper.yaml +1 -0
- package/src/adapters/medium/article.yaml +1 -0
- package/src/adapters/mermaid/render.yaml +1 -0
- package/src/adapters/minimax/tts.yaml +1 -0
- package/src/adapters/motion-studio/component-get.yaml +1 -0
- package/src/adapters/musescore/convert.yaml +2 -0
- package/src/adapters/musescore/export.yaml +2 -0
- package/src/adapters/musescore/info.yaml +1 -0
- package/src/adapters/musescore/instruments.yaml +1 -0
- package/src/adapters/musescore/transpose.yaml +1 -0
- package/src/adapters/netease-music/playlist.yaml +1 -0
- package/src/adapters/netease-music/top.yaml +1 -0
- package/src/adapters/notebooklm/get.yaml +1 -0
- package/src/adapters/notebooklm/history.yaml +1 -0
- package/src/adapters/notebooklm/note-list.yaml +1 -0
- package/src/adapters/notebooklm/notes-get.yaml +1 -0
- package/src/adapters/notebooklm/open.yaml +1 -0
- package/src/adapters/notebooklm/source-fulltext.yaml +1 -0
- package/src/adapters/notebooklm/source-get.yaml +1 -0
- package/src/adapters/notebooklm/source-guide.yaml +1 -0
- package/src/adapters/notebooklm/source-list.yaml +1 -0
- package/src/adapters/notebooklm/summary.yaml +1 -0
- package/src/adapters/novita/status.yaml +1 -0
- package/src/adapters/obs/screenshot.yaml +1 -0
- package/src/adapters/obsidian/open.yaml +1 -0
- package/src/adapters/ones/login.yaml +1 -0
- package/src/adapters/ones/task.yaml +1 -0
- package/src/adapters/pandoc/convert.yaml +1 -0
- package/src/adapters/paperreview/feedback.yaml +2 -0
- package/src/adapters/paperreview/review.yaml +2 -0
- package/src/adapters/paperreview/submit.yaml +2 -0
- package/src/adapters/pixiv/detail.yaml +1 -0
- package/src/adapters/pixiv/download.yaml +2 -0
- package/src/adapters/quark/ls.yaml +1 -0
- package/src/adapters/reddit/comments.yaml +2 -0
- package/src/adapters/renderdoc/capture-list.yaml +1 -0
- package/src/adapters/renderdoc/frame-export.yaml +1 -0
- package/src/adapters/reuters/article.yaml +2 -0
- package/src/adapters/shotcut/info.yaml +1 -0
- package/src/adapters/shotcut/render.yaml +1 -0
- package/src/adapters/sinablog/article.yaml +1 -0
- package/src/adapters/sinablog/user.yaml +1 -0
- package/src/adapters/sketch/artboards.yaml +1 -0
- package/src/adapters/sketch/export.yaml +1 -0
- package/src/adapters/sketch/symbols.yaml +1 -0
- package/src/adapters/smzdm/article.yaml +1 -0
- package/src/adapters/stackoverflow/question.yaml +1 -0
- package/src/adapters/stagehand/wrap-observe.yaml +1 -0
- package/src/adapters/steam/wishlist.yaml +1 -0
- package/src/adapters/tieba/read.yaml +1 -0
- package/src/adapters/tiktok/comment.yaml +1 -0
- package/src/adapters/tiktok/like.yaml +1 -0
- package/src/adapters/tiktok/save.yaml +1 -0
- package/src/adapters/tiktok/unlike.yaml +1 -0
- package/src/adapters/tiktok/unsave.yaml +1 -0
- package/src/adapters/twitch/streams.yaml +1 -0
- package/src/adapters/twitter/quotes.yaml +2 -0
- package/src/adapters/twitter/retweets.yaml +1 -0
- package/src/adapters/v2ex/replies.yaml +1 -0
- package/src/adapters/v2ex/topic.yaml +1 -0
- package/src/adapters/vscode/install-ext.yaml +1 -0
- package/src/adapters/vscode/open.yaml +1 -0
- package/src/adapters/web/read.yaml +1 -0
- package/src/adapters/weibo/comments.yaml +1 -0
- package/src/adapters/weibo/post.yaml +1 -0
- package/src/adapters/weixin/download.yaml +1 -0
- package/src/adapters/wiremock/create-stub.yaml +2 -0
- package/src/adapters/wiremock/delete-stub.yaml +1 -0
- package/src/adapters/wiremock/verify.yaml +1 -0
- package/src/adapters/xianyu/item.yaml +1 -0
- package/src/adapters/xiaoe/catalog.yaml +1 -0
- package/src/adapters/xiaoe/content.yaml +1 -0
- package/src/adapters/xiaoe/detail.yaml +1 -0
- package/src/adapters/xiaoe/play-url.yaml +1 -0
- package/src/adapters/xiaoyuzhou/podcast.yaml +2 -0
- package/src/adapters/yollomi/edit.yaml +2 -0
- package/src/adapters/yollomi/remove-bg.yaml +2 -0
- package/src/adapters/yollomi/restore.yaml +2 -0
- package/src/adapters/yollomi/upload.yaml +2 -0
- package/src/adapters/yollomi/upscale.yaml +2 -0
- package/src/adapters/youtube/playlist.yaml +1 -0
- package/src/adapters/yt-dlp/download.yaml +2 -0
- package/src/adapters/yt-dlp/extract-audio.yaml +2 -0
- package/src/adapters/yt-dlp/info.yaml +1 -0
- package/src/adapters/zhihu/answer.yaml +1 -0
- package/src/adapters/zhihu/answers.yaml +2 -0
- package/src/adapters/zhihu/article.yaml +1 -0
- package/src/adapters/zhihu/articles.yaml +2 -0
- package/src/adapters/zhihu/collections.yaml +2 -0
- package/src/adapters/zhihu/columns.yaml +2 -0
- package/src/adapters/zhihu/comment.yaml +1 -0
- package/src/adapters/zhihu/download.yaml +1 -0
- package/src/adapters/zhihu/followers.yaml +2 -0
- package/src/adapters/zhihu/following.yaml +2 -0
- package/src/adapters/zhihu/pins.yaml +2 -0
- package/src/adapters/zhihu/question.yaml +1 -0
- package/src/adapters/zhihu/topic.yaml +1 -0
- package/src/adapters/zhihu/topics.yaml +1 -0
- package/src/adapters/zhihu/user.yaml +2 -0
- package/src/adapters/zoom/join.yaml +1 -0
- package/src/adapters/zsxq/search.yaml +1 -0
- package/src/adapters/zsxq/topic.yaml +1 -0
- package/src/adapters/zsxq/topics.yaml +1 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,419 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP (Model Context Protocol) server for Uni-CLI.
|
|
3
|
+
* MCP (Model Context Protocol) server entry point for Uni-CLI.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Thin bootstrap: load adapters → build tool list → wire the transport →
|
|
6
|
+
* start serving. The meat lives in sibling modules:
|
|
7
|
+
* - `./tools.ts` — tool-definition builders (default/expanded/deferred)
|
|
8
|
+
* - `./dispatch.ts` — kernel-backed tool-call dispatcher
|
|
9
|
+
* - `./handler.ts` — JSON-RPC method dispatch
|
|
10
|
+
* - `./http-transport.ts` — POST /mcp transport
|
|
11
|
+
* - `./streamable-http/` — Streamable HTTP transport (MCP spec 2025-11-25)
|
|
12
|
+
*
|
|
13
|
+
* Three modes:
|
|
14
|
+
* - default (~200 tokens) — 4 meta-tools only
|
|
15
|
+
* - expanded (~160K tokens) — one tool per adapter command
|
|
16
|
+
* - deferred (~8K tokens) — stubs for ToolSearch-aware clients
|
|
11
17
|
*
|
|
12
18
|
* Three transports:
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
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.
|
|
19
|
+
* - stdio (default) — newline-delimited JSON over stdin/stdout
|
|
20
|
+
* - http — POST /mcp single request/response
|
|
21
|
+
* - streamable — Streamable HTTP with SSE session resume
|
|
19
22
|
*
|
|
20
23
|
* Auth pass-through is automatic: every adapter the CLI loads (including
|
|
21
24
|
* cookie-based ones) is exposed by name; the runtime resolves cookies on
|
|
22
25
|
* each call via the same code path as the CLI.
|
|
23
26
|
*/
|
|
24
27
|
import { createInterface } from "node:readline";
|
|
25
|
-
import { createServer, } from "node:http";
|
|
26
28
|
import { loadAllAdapters, loadTsAdapters } from "../discovery/loader.js";
|
|
27
|
-
import { getAllAdapters, listCommands
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import { startStreamableHttp } from "./streamable-http.js";
|
|
33
|
-
|
|
34
|
-
import { buildInputSchema, buildOutputSchema, buildToolName, truncateDescription, } from "./schema.js";
|
|
35
|
-
import { resolveElicitation } from "./elicitation.js";
|
|
36
|
-
// ── Smart default tools (4 meta-tools — the default mode) ─────────────────
|
|
37
|
-
const MAX_RESULT_SIZE_CHARS = 10_000;
|
|
38
|
-
function buildDefaultTools() {
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
name: "unicli_run",
|
|
42
|
-
description: "Execute any Uni-CLI command. Returns JSON results.",
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
site: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "Site name (e.g. hackernews, github, bilibili)",
|
|
49
|
-
},
|
|
50
|
-
command: {
|
|
51
|
-
type: "string",
|
|
52
|
-
description: "Command to run (e.g. top, search, hot)",
|
|
53
|
-
},
|
|
54
|
-
args: {
|
|
55
|
-
type: "object",
|
|
56
|
-
description: 'Key-value arguments (e.g. {"query": "ai", "limit": 10})',
|
|
57
|
-
additionalProperties: true,
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
required: ["site", "command"],
|
|
61
|
-
},
|
|
62
|
-
_meta: {
|
|
63
|
-
"anthropic/searchHint": "Execute CLI commands on 200+ websites and desktop apps. Run adapters by site and command name.",
|
|
64
|
-
},
|
|
65
|
-
annotations: {
|
|
66
|
-
readOnlyHint: false,
|
|
67
|
-
destructiveHint: false,
|
|
68
|
-
idempotentHint: false,
|
|
69
|
-
openWorldHint: true,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: "unicli_list",
|
|
74
|
-
description: "List available commands. Filter by site or adapter type.",
|
|
75
|
-
inputSchema: {
|
|
76
|
-
type: "object",
|
|
77
|
-
properties: {
|
|
78
|
-
site: {
|
|
79
|
-
type: "string",
|
|
80
|
-
description: "Filter by site name (partial match)",
|
|
81
|
-
},
|
|
82
|
-
type: {
|
|
83
|
-
type: "string",
|
|
84
|
-
description: "Filter by adapter type",
|
|
85
|
-
enum: ["web-api", "desktop", "browser", "bridge", "service"],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
_meta: {
|
|
90
|
-
"anthropic/searchHint": "Browse available Uni-CLI sites and commands. Filter by site name or adapter type.",
|
|
91
|
-
"anthropic/alwaysLoad": true,
|
|
92
|
-
},
|
|
93
|
-
annotations: {
|
|
94
|
-
readOnlyHint: true,
|
|
95
|
-
idempotentHint: true,
|
|
96
|
-
openWorldHint: false,
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: "unicli_search",
|
|
101
|
-
description: "Search 200+ sites and 956 commands by intent. Bilingual (EN/ZH). Returns top matches with usage examples.",
|
|
102
|
-
inputSchema: {
|
|
103
|
-
type: "object",
|
|
104
|
-
properties: {
|
|
105
|
-
query: {
|
|
106
|
-
type: "string",
|
|
107
|
-
description: "Natural language intent (e.g. 'download video', '推特热门', 'stock price')",
|
|
108
|
-
},
|
|
109
|
-
limit: {
|
|
110
|
-
type: "integer",
|
|
111
|
-
description: "Max results (default 5)",
|
|
112
|
-
default: 5,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
required: ["query"],
|
|
116
|
-
},
|
|
117
|
-
_meta: {
|
|
118
|
-
"anthropic/searchHint": "Find CLI commands by intent. Semantic search across websites, desktop apps, macOS. Bilingual Chinese/English.",
|
|
119
|
-
"anthropic/alwaysLoad": true,
|
|
120
|
-
},
|
|
121
|
-
annotations: {
|
|
122
|
-
readOnlyHint: true,
|
|
123
|
-
idempotentHint: true,
|
|
124
|
-
openWorldHint: false,
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "unicli_explore",
|
|
129
|
-
description: "Auto-discover API endpoints for any URL. Navigates the page, captures network requests, generates YAML adapters.",
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {
|
|
133
|
-
url: { type: "string", description: "Website URL to explore" },
|
|
134
|
-
goal: {
|
|
135
|
-
type: "string",
|
|
136
|
-
description: "Capability to find (e.g. 'search', 'hot', 'feed')",
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
required: ["url"],
|
|
140
|
-
},
|
|
141
|
-
_meta: {
|
|
142
|
-
"anthropic/searchHint": "Auto-discover API endpoints for any website URL. Generate YAML adapters for new sites.",
|
|
143
|
-
},
|
|
144
|
-
annotations: {
|
|
145
|
-
readOnlyHint: false,
|
|
146
|
-
destructiveHint: false,
|
|
147
|
-
idempotentHint: false,
|
|
148
|
-
openWorldHint: true,
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
}
|
|
153
|
-
const expandedRegistry = new Map();
|
|
154
|
-
/**
|
|
155
|
-
* Build the expanded tool set: 4 default meta-tools + one full tool per
|
|
156
|
-
* adapter command. Clients see the complete Uni-CLI surface area.
|
|
157
|
-
*
|
|
158
|
-
* Token cost: ~160K for 956 commands. Use only when the client can handle it.
|
|
159
|
-
*/
|
|
160
|
-
function buildExpandedTools() {
|
|
161
|
-
const tools = [];
|
|
162
|
-
tools.push(...buildDefaultTools());
|
|
163
|
-
expandedRegistry.clear();
|
|
164
|
-
const seen = new Set(DEFAULT_TOOL_NAMES);
|
|
165
|
-
for (const adapter of getAllAdapters()) {
|
|
166
|
-
for (const [cmdName, cmd] of Object.entries(adapter.commands)) {
|
|
167
|
-
const rawDesc = cmd.description?.trim() ||
|
|
168
|
-
adapter.description?.trim() ||
|
|
169
|
-
`${cmdName} for ${adapter.name}`;
|
|
170
|
-
const toolName = buildToolName(adapter.name, cmdName);
|
|
171
|
-
if (seen.has(toolName)) {
|
|
172
|
-
process.stderr.write(`unicli MCP: tool name collision: ${toolName} — shadowing ${adapter.name}/${cmdName}\n`);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
seen.add(toolName);
|
|
176
|
-
expandedRegistry.set(toolName, { adapter, cmdName, cmd });
|
|
177
|
-
tools.push({
|
|
178
|
-
name: toolName,
|
|
179
|
-
description: truncateDescription(`[${adapter.name}] ${rawDesc}`),
|
|
180
|
-
inputSchema: buildInputSchema(cmd),
|
|
181
|
-
outputSchema: buildOutputSchema(cmd),
|
|
182
|
-
_meta: {
|
|
183
|
-
"anthropic/searchHint": `${adapter.name}: ${rawDesc}`,
|
|
184
|
-
},
|
|
185
|
-
annotations: {
|
|
186
|
-
readOnlyHint: true,
|
|
187
|
-
idempotentHint: true,
|
|
188
|
-
openWorldHint: true,
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return tools;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Build deferred tool set: 4 default meta-tools with full schemas, plus
|
|
197
|
-
* lightweight stubs for all adapter commands (name + searchHint only,
|
|
198
|
-
* minimal inputSchema). Clients like Claude Code's ToolSearch can discover
|
|
199
|
-
* tools by searchHint and then call them — the handler resolves the full
|
|
200
|
-
* command at call time via the expandedRegistry.
|
|
201
|
-
*
|
|
202
|
-
* Token cost: ~8K (vs ~160K for expanded). 95% reduction.
|
|
203
|
-
*/
|
|
204
|
-
function buildDeferredTools() {
|
|
205
|
-
const tools = [];
|
|
206
|
-
tools.push(...buildDefaultTools());
|
|
207
|
-
expandedRegistry.clear();
|
|
208
|
-
const seen = new Set(DEFAULT_TOOL_NAMES);
|
|
209
|
-
for (const adapter of getAllAdapters()) {
|
|
210
|
-
for (const [cmdName, cmd] of Object.entries(adapter.commands)) {
|
|
211
|
-
const rawDesc = cmd.description?.trim() ||
|
|
212
|
-
adapter.description?.trim() ||
|
|
213
|
-
`${cmdName} for ${adapter.name}`;
|
|
214
|
-
const toolName = buildToolName(adapter.name, cmdName);
|
|
215
|
-
if (seen.has(toolName))
|
|
216
|
-
continue;
|
|
217
|
-
seen.add(toolName);
|
|
218
|
-
// Register in the lookup table for runtime dispatch
|
|
219
|
-
expandedRegistry.set(toolName, { adapter, cmdName, cmd });
|
|
220
|
-
// Lightweight stub: name + searchHint + minimal schema.
|
|
221
|
-
// Full inputSchema is resolved at call time via expandedRegistry.
|
|
222
|
-
tools.push({
|
|
223
|
-
name: toolName,
|
|
224
|
-
description: truncateDescription(`[${adapter.name}] ${rawDesc}`),
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: "object",
|
|
227
|
-
properties: {
|
|
228
|
-
_args: {
|
|
229
|
-
type: "object",
|
|
230
|
-
description: "Command arguments (pass key-value pairs)",
|
|
231
|
-
additionalProperties: true,
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
_meta: {
|
|
236
|
-
"anthropic/searchHint": `${adapter.name}: ${rawDesc}`,
|
|
237
|
-
},
|
|
238
|
-
annotations: {
|
|
239
|
-
readOnlyHint: true,
|
|
240
|
-
idempotentHint: true,
|
|
241
|
-
openWorldHint: true,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return tools;
|
|
247
|
-
}
|
|
248
|
-
const DEFAULT_TOOL_NAMES = new Set([
|
|
249
|
-
"unicli_run",
|
|
250
|
-
"unicli_list",
|
|
251
|
-
"unicli_search",
|
|
252
|
-
"unicli_explore",
|
|
253
|
-
"unicli_discover",
|
|
254
|
-
]);
|
|
255
|
-
// ── Tool Handlers ───────────────────────────────────────────────────────────
|
|
256
|
-
function handleListAdapters(params) {
|
|
257
|
-
let commands = listCommands();
|
|
258
|
-
const site = params.site;
|
|
259
|
-
const type = params.type;
|
|
260
|
-
if (site) {
|
|
261
|
-
commands = commands.filter((c) => c.site.includes(site));
|
|
262
|
-
}
|
|
263
|
-
if (type) {
|
|
264
|
-
commands = commands.filter((c) => c.type === type);
|
|
265
|
-
}
|
|
266
|
-
const adapters = getAllAdapters();
|
|
267
|
-
const siteMap = new Map();
|
|
268
|
-
for (const cmd of commands) {
|
|
269
|
-
let entry = siteMap.get(cmd.site);
|
|
270
|
-
if (!entry) {
|
|
271
|
-
const adapter = adapters.find((a) => a.name === cmd.site);
|
|
272
|
-
entry = { type: adapter?.type ?? cmd.type, commands: [] };
|
|
273
|
-
siteMap.set(cmd.site, entry);
|
|
274
|
-
}
|
|
275
|
-
entry.commands.push({ name: cmd.command, description: cmd.description });
|
|
276
|
-
}
|
|
277
|
-
const result = Array.from(siteMap.entries()).map(([name, info]) => ({
|
|
278
|
-
site: name,
|
|
279
|
-
type: info.type,
|
|
280
|
-
commands: info.commands,
|
|
281
|
-
}));
|
|
282
|
-
const data = {
|
|
283
|
-
total_sites: result.length,
|
|
284
|
-
total_commands: commands.length,
|
|
285
|
-
adapters: result,
|
|
286
|
-
};
|
|
287
|
-
return {
|
|
288
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
289
|
-
structuredContent: { type: "json", data },
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
async function runResolvedCommand(adapter, cmd, cmdName, args) {
|
|
293
|
-
// Merge default args
|
|
294
|
-
const mergedArgs = { limit: 20, ...args };
|
|
295
|
-
if (args.limit !== undefined) {
|
|
296
|
-
mergedArgs.limit =
|
|
297
|
-
typeof args.limit === "number"
|
|
298
|
-
? args.limit
|
|
299
|
-
: parseInt(String(args.limit), 10) || 20;
|
|
300
|
-
}
|
|
301
|
-
try {
|
|
302
|
-
let results;
|
|
303
|
-
if (cmd.pipeline) {
|
|
304
|
-
results = await runPipeline(cmd.pipeline, mergedArgs, adapter.base, {
|
|
305
|
-
site: adapter.name,
|
|
306
|
-
strategy: adapter.strategy,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
else if (cmd.func) {
|
|
310
|
-
const raw = await cmd.func(null, mergedArgs);
|
|
311
|
-
results = Array.isArray(raw) ? raw : [raw];
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
const errorData = {
|
|
315
|
-
error: "No pipeline or function defined for this command",
|
|
316
|
-
};
|
|
317
|
-
return {
|
|
318
|
-
content: [{ type: "text", text: JSON.stringify(errorData) }],
|
|
319
|
-
structuredContent: { type: "json", data: errorData },
|
|
320
|
-
isError: true,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
const data = { count: results.length, results };
|
|
324
|
-
return {
|
|
325
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
326
|
-
structuredContent: { type: "json", data },
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
catch (err) {
|
|
330
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
331
|
-
const errorData = {
|
|
332
|
-
error: message,
|
|
333
|
-
adapter_path: `src/adapters/${adapter.name}/${cmdName}.yaml`,
|
|
334
|
-
suggestion: "The adapter may need updating. Check the YAML file.",
|
|
335
|
-
};
|
|
336
|
-
return {
|
|
337
|
-
content: [{ type: "text", text: JSON.stringify(errorData, null, 2) }],
|
|
338
|
-
structuredContent: { type: "json", data: errorData },
|
|
339
|
-
isError: true,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Annotate a tool result with `_meta.anthropic/maxResultSizeChars` when the
|
|
345
|
-
* serialized payload exceeds MAX_RESULT_SIZE_CHARS (10 KB). This tells
|
|
346
|
-
* Claude Code to accept large payloads without truncation.
|
|
347
|
-
*/
|
|
348
|
-
export function annotateIfLarge(result) {
|
|
349
|
-
const totalChars = result.content.reduce((sum, c) => sum + c.text.length, 0);
|
|
350
|
-
if (totalChars > MAX_RESULT_SIZE_CHARS) {
|
|
351
|
-
return {
|
|
352
|
-
...result,
|
|
353
|
-
_meta: { "anthropic/maxResultSizeChars": 100_000 },
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
async function handleRunCommand(params) {
|
|
359
|
-
const site = params.site;
|
|
360
|
-
const command = params.command;
|
|
361
|
-
const args = params.args ?? {};
|
|
362
|
-
if (!site || !command) {
|
|
363
|
-
const errorData = { error: "site and command are required" };
|
|
364
|
-
return {
|
|
365
|
-
content: [{ type: "text", text: JSON.stringify(errorData) }],
|
|
366
|
-
structuredContent: { type: "json", data: errorData },
|
|
367
|
-
isError: true,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
const resolved = resolveCommand(site, command);
|
|
371
|
-
if (!resolved) {
|
|
372
|
-
const adapters = getAllAdapters();
|
|
373
|
-
const matchingSites = adapters
|
|
374
|
-
.filter((a) => a.name.includes(site))
|
|
375
|
-
.map((a) => ({
|
|
376
|
-
site: a.name,
|
|
377
|
-
commands: Object.keys(a.commands),
|
|
378
|
-
}));
|
|
379
|
-
const errorData = {
|
|
380
|
-
error: `Unknown command: ${site} ${command}`,
|
|
381
|
-
suggestion: matchingSites.length > 0
|
|
382
|
-
? `Did you mean one of these? ${JSON.stringify(matchingSites)}`
|
|
383
|
-
: "Use list_adapters to see all available commands.",
|
|
384
|
-
};
|
|
385
|
-
return {
|
|
386
|
-
content: [{ type: "text", text: JSON.stringify(errorData, null, 2) }],
|
|
387
|
-
structuredContent: { type: "json", data: errorData },
|
|
388
|
-
isError: true,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
return runResolvedCommand(resolved.adapter, resolved.command, command, args);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Expanded-tool dispatcher — parse `unicli_<site>_<command>` back to its
|
|
395
|
-
* components and call the resolver. Returns `undefined` when the tool name
|
|
396
|
-
* is not in expanded form, so the caller can fall through to default-tool
|
|
397
|
-
* handling (list_adapters / run_command).
|
|
398
|
-
*/
|
|
399
|
-
async function handleExpandedTool(toolName, args) {
|
|
400
|
-
if (!toolName.startsWith("unicli_"))
|
|
401
|
-
return undefined;
|
|
402
|
-
// Dictionary lookup into the expansion registry built by
|
|
403
|
-
// `buildExpandedTools`. This is the ONLY correct way to map a normalized
|
|
404
|
-
// tool name back to its adapter + command because the normalization
|
|
405
|
-
// (`s/[^a-zA-Z0-9_]/_/g`) is not reversible — a command file named
|
|
406
|
-
// `capture-list.yaml` and another named `capture_list.yaml` would map
|
|
407
|
-
// to the same tool name. The registry resolves the ambiguity deterministically
|
|
408
|
-
// (first-write-wins, collisions logged to stderr in `buildExpandedTools`).
|
|
409
|
-
const entry = expandedRegistry.get(toolName);
|
|
410
|
-
if (!entry)
|
|
411
|
-
return undefined;
|
|
412
|
-
return runResolvedCommand(entry.adapter, entry.cmd, entry.cmdName, args);
|
|
413
|
-
}
|
|
414
|
-
// ── MCP Protocol Handler ────────────────────────────────────────────────────
|
|
415
|
-
// Protocol version imported from src/constants.ts (single source of truth)
|
|
416
|
-
const PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
|
|
29
|
+
import { getAllAdapters, listCommands } from "../registry.js";
|
|
30
|
+
import { VERSION } from "../constants.js";
|
|
31
|
+
import { buildDefaultTools, buildExpandedTools, buildDeferredTools, } from "./tools.js";
|
|
32
|
+
import { buildHandler, } from "./handler.js";
|
|
33
|
+
import { startHttp } from "./http-transport.js";
|
|
34
|
+
import { startStreamableHttp } from "./streamable-http/index.js";
|
|
35
|
+
export { annotateIfLarge } from "./dispatch.js";
|
|
417
36
|
function parseArgs(argv) {
|
|
418
37
|
const opts = {
|
|
419
38
|
expanded: false,
|
|
@@ -445,223 +64,11 @@ function parseArgs(argv) {
|
|
|
445
64
|
}
|
|
446
65
|
return opts;
|
|
447
66
|
}
|
|
448
|
-
function buildHandler(tools) {
|
|
449
|
-
return function handleRequest(req) {
|
|
450
|
-
const id = req.id ?? null;
|
|
451
|
-
switch (req.method) {
|
|
452
|
-
case "initialize":
|
|
453
|
-
return {
|
|
454
|
-
jsonrpc: "2.0",
|
|
455
|
-
id,
|
|
456
|
-
result: {
|
|
457
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
458
|
-
capabilities: {
|
|
459
|
-
tools: { listChanged: false },
|
|
460
|
-
elicitation: { supported: true },
|
|
461
|
-
},
|
|
462
|
-
serverInfo: {
|
|
463
|
-
name: "unicli",
|
|
464
|
-
version: VERSION,
|
|
465
|
-
},
|
|
466
|
-
},
|
|
467
|
-
};
|
|
468
|
-
case "notifications/initialized":
|
|
469
|
-
// JSON-RPC notifications must not receive responses.
|
|
470
|
-
// Returning a sentinel that transports check before serializing.
|
|
471
|
-
return null;
|
|
472
|
-
case "tools/list":
|
|
473
|
-
return {
|
|
474
|
-
jsonrpc: "2.0",
|
|
475
|
-
id,
|
|
476
|
-
result: { tools },
|
|
477
|
-
};
|
|
478
|
-
case "tools/call": {
|
|
479
|
-
const params = req.params;
|
|
480
|
-
if (!params?.name) {
|
|
481
|
-
return {
|
|
482
|
-
jsonrpc: "2.0",
|
|
483
|
-
id,
|
|
484
|
-
error: { code: -32602, message: "Missing tool name" },
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
const toolArgs = params.arguments ?? {};
|
|
488
|
-
switch (params.name) {
|
|
489
|
-
// Support both old names (list_adapters, run_command) and new
|
|
490
|
-
// names (unicli_list, unicli_run) for backwards compatibility.
|
|
491
|
-
case "unicli_list":
|
|
492
|
-
case "list_adapters": {
|
|
493
|
-
const result = handleListAdapters(toolArgs);
|
|
494
|
-
return { jsonrpc: "2.0", id, result: annotateIfLarge(result) };
|
|
495
|
-
}
|
|
496
|
-
case "unicli_run":
|
|
497
|
-
case "run_command":
|
|
498
|
-
return handleRunCommand(toolArgs).then((result) => ({
|
|
499
|
-
jsonrpc: "2.0",
|
|
500
|
-
id,
|
|
501
|
-
result: annotateIfLarge(result),
|
|
502
|
-
}));
|
|
503
|
-
case "unicli_search": {
|
|
504
|
-
const searchQuery = toolArgs.query;
|
|
505
|
-
const searchLimit = toolArgs.limit || 5;
|
|
506
|
-
if (!searchQuery) {
|
|
507
|
-
return {
|
|
508
|
-
jsonrpc: "2.0",
|
|
509
|
-
id,
|
|
510
|
-
error: {
|
|
511
|
-
code: -32602,
|
|
512
|
-
message: "Missing required parameter: query",
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
return import("../discovery/search.js").then(({ search: searchFn }) => {
|
|
517
|
-
const results = searchFn(searchQuery, searchLimit);
|
|
518
|
-
const data = {
|
|
519
|
-
query: searchQuery,
|
|
520
|
-
count: results.length,
|
|
521
|
-
results: results.map((r) => ({
|
|
522
|
-
command: `unicli ${r.site} ${r.command}`,
|
|
523
|
-
site: r.site,
|
|
524
|
-
name: r.command,
|
|
525
|
-
description: r.description,
|
|
526
|
-
score: r.score,
|
|
527
|
-
category: r.category,
|
|
528
|
-
usage: r.usage,
|
|
529
|
-
})),
|
|
530
|
-
};
|
|
531
|
-
return {
|
|
532
|
-
jsonrpc: "2.0",
|
|
533
|
-
id,
|
|
534
|
-
result: annotateIfLarge({
|
|
535
|
-
content: [
|
|
536
|
-
{
|
|
537
|
-
type: "text",
|
|
538
|
-
text: JSON.stringify(data, null, 2),
|
|
539
|
-
},
|
|
540
|
-
],
|
|
541
|
-
structuredContent: { type: "json", data },
|
|
542
|
-
}),
|
|
543
|
-
};
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
case "unicli_explore":
|
|
547
|
-
case "unicli_discover": {
|
|
548
|
-
// unicli_explore is the canonical name (v0.211.1+).
|
|
549
|
-
// unicli_discover kept as alias for backwards compatibility.
|
|
550
|
-
const discoverUrl = toolArgs.url;
|
|
551
|
-
const discoverGoal = toolArgs.goal;
|
|
552
|
-
if (!discoverUrl) {
|
|
553
|
-
return {
|
|
554
|
-
jsonrpc: "2.0",
|
|
555
|
-
id,
|
|
556
|
-
error: {
|
|
557
|
-
code: -32602,
|
|
558
|
-
message: "Missing required parameter: url",
|
|
559
|
-
},
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
if (!discoverUrl.startsWith("http://") &&
|
|
563
|
-
!discoverUrl.startsWith("https://")) {
|
|
564
|
-
return {
|
|
565
|
-
jsonrpc: "2.0",
|
|
566
|
-
id,
|
|
567
|
-
error: {
|
|
568
|
-
code: -32602,
|
|
569
|
-
message: "URL must start with http:// or https://",
|
|
570
|
-
},
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
return import("node:child_process").then(({ execFile: ef }) => import("node:util").then(({ promisify: prom }) => {
|
|
574
|
-
const execFileP = prom(ef);
|
|
575
|
-
const discoverArgs = ["generate", discoverUrl, "--json"];
|
|
576
|
-
if (discoverGoal)
|
|
577
|
-
discoverArgs.push("--goal", discoverGoal);
|
|
578
|
-
return execFileP("unicli", discoverArgs, {
|
|
579
|
-
timeout: 120_000,
|
|
580
|
-
encoding: "utf-8",
|
|
581
|
-
}).then(({ stdout }) => ({
|
|
582
|
-
jsonrpc: "2.0",
|
|
583
|
-
id,
|
|
584
|
-
result: annotateIfLarge({
|
|
585
|
-
content: [{ type: "text", text: stdout }],
|
|
586
|
-
}),
|
|
587
|
-
}), (err) => ({
|
|
588
|
-
jsonrpc: "2.0",
|
|
589
|
-
id,
|
|
590
|
-
result: annotateIfLarge({
|
|
591
|
-
content: [
|
|
592
|
-
{
|
|
593
|
-
type: "text",
|
|
594
|
-
text: JSON.stringify({
|
|
595
|
-
error: err instanceof Error ? err.message : String(err),
|
|
596
|
-
}),
|
|
597
|
-
},
|
|
598
|
-
],
|
|
599
|
-
isError: true,
|
|
600
|
-
}),
|
|
601
|
-
}));
|
|
602
|
-
}));
|
|
603
|
-
}
|
|
604
|
-
default:
|
|
605
|
-
return handleExpandedTool(params.name, toolArgs).then((result) => {
|
|
606
|
-
if (result)
|
|
607
|
-
return { jsonrpc: "2.0", id, result: annotateIfLarge(result) };
|
|
608
|
-
return {
|
|
609
|
-
jsonrpc: "2.0",
|
|
610
|
-
id,
|
|
611
|
-
error: {
|
|
612
|
-
code: -32602,
|
|
613
|
-
message: `Unknown tool: ${params.name}. Use unicli_list to see available commands.`,
|
|
614
|
-
},
|
|
615
|
-
};
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
case "elicitation/response": {
|
|
620
|
-
const elicitParams = req.params;
|
|
621
|
-
if (elicitParams?.id == null || !elicitParams?.response) {
|
|
622
|
-
return {
|
|
623
|
-
jsonrpc: "2.0",
|
|
624
|
-
id,
|
|
625
|
-
error: {
|
|
626
|
-
code: -32602,
|
|
627
|
-
message: "Missing id or response in elicitation/response",
|
|
628
|
-
},
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
const resolved = resolveElicitation(elicitParams.id, elicitParams.response);
|
|
632
|
-
return {
|
|
633
|
-
jsonrpc: "2.0",
|
|
634
|
-
id,
|
|
635
|
-
result: { resolved },
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
case "ping":
|
|
639
|
-
return { jsonrpc: "2.0", id, result: {} };
|
|
640
|
-
default:
|
|
641
|
-
if (id !== null && id !== undefined) {
|
|
642
|
-
return {
|
|
643
|
-
jsonrpc: "2.0",
|
|
644
|
-
id,
|
|
645
|
-
error: {
|
|
646
|
-
code: -32601,
|
|
647
|
-
message: `Method not found: ${req.method}`,
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
// ── Stdio Transport ─────────────────────────────────────────────────────────
|
|
656
67
|
function send(response) {
|
|
657
|
-
|
|
658
|
-
process.stdout.write(json + "\n");
|
|
68
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
659
69
|
}
|
|
660
70
|
async function startStdio(handler) {
|
|
661
|
-
const rl = createInterface({
|
|
662
|
-
input: process.stdin,
|
|
663
|
-
terminal: false,
|
|
664
|
-
});
|
|
71
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
665
72
|
rl.on("line", async (line) => {
|
|
666
73
|
const trimmed = line.trim();
|
|
667
74
|
if (!trimmed)
|
|
@@ -680,9 +87,8 @@ async function startStdio(handler) {
|
|
|
680
87
|
}
|
|
681
88
|
try {
|
|
682
89
|
const response = await handler(req);
|
|
683
|
-
if (response)
|
|
90
|
+
if (response)
|
|
684
91
|
send(response);
|
|
685
|
-
}
|
|
686
92
|
}
|
|
687
93
|
catch (err) {
|
|
688
94
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -697,133 +103,14 @@ async function startStdio(handler) {
|
|
|
697
103
|
process.exit(0);
|
|
698
104
|
});
|
|
699
105
|
}
|
|
700
|
-
// ── HTTP Transport ──────────────────────────────────────────────────────────
|
|
701
|
-
/**
|
|
702
|
-
* Simple JSON-RPC over HTTP. POST /mcp accepts a single JSON-RPC envelope and
|
|
703
|
-
* returns a single JSON response. GET /mcp returns server info — handy for
|
|
704
|
-
* a health check from a browser.
|
|
705
|
-
*
|
|
706
|
-
* Note: this is intentionally NOT a full MCP Streamable HTTP transport —
|
|
707
|
-
* no SSE event stream, no session resume. Most clients that "speak HTTP"
|
|
708
|
-
* to MCP only need request/response, and starting with the simpler shape
|
|
709
|
-
* means zero new dependencies and a tiny attack surface.
|
|
710
|
-
*/
|
|
711
|
-
async function startHttp(handler, port, authEnabled = false) {
|
|
712
|
-
const oauthMiddleware = authEnabled ? createOAuthMiddleware() : null;
|
|
713
|
-
const server = createServer((req, res) => {
|
|
714
|
-
// OAuth routes (authorize + token) — always public
|
|
715
|
-
if (authEnabled && handleOAuthRoute(req, res))
|
|
716
|
-
return;
|
|
717
|
-
// Health endpoint — always public
|
|
718
|
-
if (req.method === "GET" &&
|
|
719
|
-
(req.url === "/" || req.url === "/mcp" || req.url === "/health")) {
|
|
720
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
721
|
-
const adapterCount = getAllAdapters().length;
|
|
722
|
-
const commandCount = listCommands().length;
|
|
723
|
-
// Compute actual expanded tool count: 3 default tools + all adapter commands.
|
|
724
|
-
let expandedCount = 3; // default tools
|
|
725
|
-
for (const adapter of getAllAdapters()) {
|
|
726
|
-
expandedCount += Object.keys(adapter.commands).length;
|
|
727
|
-
}
|
|
728
|
-
res.end(JSON.stringify({
|
|
729
|
-
status: "ok",
|
|
730
|
-
adapters: adapterCount,
|
|
731
|
-
commands: commandCount,
|
|
732
|
-
tools: { default: 3, expanded: expandedCount },
|
|
733
|
-
version: VERSION,
|
|
734
|
-
}));
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (req.method !== "POST" || req.url !== "/mcp") {
|
|
738
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
739
|
-
res.end(JSON.stringify({ error: "POST /mcp" }));
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
// OAuth middleware — block unauthenticated requests when --auth is set
|
|
743
|
-
if (oauthMiddleware?.(req, res))
|
|
744
|
-
return;
|
|
745
|
-
const MAX_BODY = 1_048_576; // 1 MB
|
|
746
|
-
const chunks = [];
|
|
747
|
-
let bodySize = 0;
|
|
748
|
-
let aborted = false;
|
|
749
|
-
req.on("data", (chunk) => {
|
|
750
|
-
bodySize += chunk.length;
|
|
751
|
-
if (bodySize > MAX_BODY) {
|
|
752
|
-
aborted = true;
|
|
753
|
-
res.writeHead(413, { "Content-Type": "application/json" });
|
|
754
|
-
res.end(JSON.stringify({
|
|
755
|
-
jsonrpc: "2.0",
|
|
756
|
-
id: null,
|
|
757
|
-
error: { code: -32600, message: "Request too large" },
|
|
758
|
-
}));
|
|
759
|
-
req.destroy();
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
chunks.push(chunk);
|
|
763
|
-
});
|
|
764
|
-
req.on("end", async () => {
|
|
765
|
-
if (aborted)
|
|
766
|
-
return;
|
|
767
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
768
|
-
let parsed;
|
|
769
|
-
try {
|
|
770
|
-
parsed = JSON.parse(body);
|
|
771
|
-
}
|
|
772
|
-
catch {
|
|
773
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
774
|
-
res.end(JSON.stringify({
|
|
775
|
-
jsonrpc: "2.0",
|
|
776
|
-
id: null,
|
|
777
|
-
error: { code: -32700, message: "Parse error" },
|
|
778
|
-
}));
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
try {
|
|
782
|
-
const response = await handler(parsed);
|
|
783
|
-
if (!response) {
|
|
784
|
-
// JSON-RPC notification — no response expected
|
|
785
|
-
res.writeHead(204);
|
|
786
|
-
res.end();
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
790
|
-
res.end(JSON.stringify(response));
|
|
791
|
-
}
|
|
792
|
-
catch (err) {
|
|
793
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
794
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
795
|
-
res.end(JSON.stringify({
|
|
796
|
-
jsonrpc: "2.0",
|
|
797
|
-
id: parsed.id ?? null,
|
|
798
|
-
error: { code: -32603, message: `Internal error: ${message}` },
|
|
799
|
-
}));
|
|
800
|
-
}
|
|
801
|
-
});
|
|
802
|
-
});
|
|
803
|
-
await new Promise((resolve, reject) => {
|
|
804
|
-
server.once("error", reject);
|
|
805
|
-
server.listen(port, "127.0.0.1", () => {
|
|
806
|
-
server.off("error", reject);
|
|
807
|
-
resolve();
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
process.stderr.write(`unicli MCP server v${VERSION} — HTTP transport on http://127.0.0.1:${port}/mcp\n`);
|
|
811
|
-
}
|
|
812
|
-
// ── main ────────────────────────────────────────────────────────────────────
|
|
813
106
|
async function main() {
|
|
814
107
|
const opts = parseArgs(process.argv.slice(2));
|
|
815
|
-
// Load adapters (same as CLI)
|
|
816
108
|
loadAllAdapters();
|
|
817
109
|
await loadTsAdapters();
|
|
818
|
-
// Three modes:
|
|
819
|
-
// default → 4 meta-tools (~200 tokens)
|
|
820
|
-
// expanded → 4 meta-tools + 956 full tool schemas (~160K tokens)
|
|
821
|
-
// deferred → 4 meta-tools + 956 lightweight stubs (~8K tokens)
|
|
822
110
|
const mode = opts.expanded ? "expanded" : "default";
|
|
823
111
|
const tools = opts.expanded ? buildExpandedTools() : buildDefaultTools();
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
// For explicit control, the expanded flag takes precedence.
|
|
112
|
+
// Streamable HTTP auto-activates deferred mode — remote clients benefit
|
|
113
|
+
// most from searchHint-based discovery.
|
|
827
114
|
if (opts.transport === "streamable" && !opts.expanded) {
|
|
828
115
|
const deferredTools = buildDeferredTools();
|
|
829
116
|
tools.length = 0;
|
|
@@ -839,12 +126,14 @@ async function main() {
|
|
|
839
126
|
return;
|
|
840
127
|
}
|
|
841
128
|
if (opts.transport === "streamable") {
|
|
129
|
+
// v0.213.3 P3: streamable-http.Handler now returns
|
|
130
|
+
// `Promise<JsonRpcResponse | undefined>`, so the pre-P3 cast-adapt
|
|
131
|
+
// is gone — the types match the `undefined`-for-notification contract.
|
|
842
132
|
await startStreamableHttp(opts.port, handler, { auth: opts.auth });
|
|
843
133
|
const authLabel = opts.auth ? ", OAuth enabled" : "";
|
|
844
134
|
process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools, mode=${mode}, transport=streamable${authLabel})\n`);
|
|
845
135
|
return;
|
|
846
136
|
}
|
|
847
|
-
// stdio (default)
|
|
848
137
|
await startStdio(handler);
|
|
849
138
|
process.stderr.write(`unicli MCP server v${VERSION} — ${adapterCount} sites, ${commandCount} commands (${tools.length} tools registered, mode=${mode})\n`);
|
|
850
139
|
}
|