shadow-claw 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/main/skill-creator/SKILL.md +39 -0
- package/.agents/skills/main/skill-creator/references/declarative-pipelines.md +44 -0
- package/.agents/skills/main/skill-creator/references/declarative-tools.md +35 -0
- package/.agents/skills/main/skill-creator/references/frontmatter-spec.md +50 -0
- package/.well-known/agent-skills/index.json +14 -0
- package/README.md +5 -3
- package/bin/build/build.mjs +146 -15
- package/bin/build/build.test.mjs +57 -0
- package/bin/cli.mjs +26 -0
- package/bin/commands/mcp.mjs +277 -55
- package/bin/commands/mcp.test.mjs +275 -1
- package/bin/commands/skills-index.mjs +328 -0
- package/bin/commands/skills-index.test.mjs +161 -0
- package/bin/site-config/utils/custom-elements/get-approved-custom-element-scripts.test.mjs +2 -0
- package/bin/site-config/utils/custom-elements/sanitize-embedded-custom-element-scripts.test.mjs +10 -1
- package/dist/lib/components/index.js +25 -25
- package/dist/lib/components/settings/shadow-claw-accounts.js +1 -1
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-integrations.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw-a2ui.js +1 -1
- package/dist/lib/components/shadow-claw-accounts.js +1 -1
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-integrations.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-mcp-remote.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-page-header.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-CrlGan0m.js → configurePeerJs-oODzltaC.js} +2 -2
- package/dist/lib/{configurePeerJs-CrlGan0m.js.map → configurePeerJs-oODzltaC.js.map} +1 -1
- package/dist/lib/core/control-plane-client.d.ts +1 -0
- package/dist/lib/{downloadGroupFile-DboUjmp2.js → downloadGroupFile-DmcBQU1n.js} +2 -2
- package/dist/lib/{downloadGroupFile-DboUjmp2.js.map → downloadGroupFile-DmcBQU1n.js.map} +1 -1
- package/dist/lib/{file-viewer-BrK2Wxgh.js → file-viewer-C3YKnq_-.js} +4 -3
- package/dist/lib/{file-viewer-BrK2Wxgh.js.map → file-viewer-C3YKnq_-.js.map} +1 -1
- package/dist/lib/index.js +26 -26
- package/dist/lib/{initControlPlane-CsO4UR6_.js → initControlPlane-C2zURVIS.js} +86 -13
- package/dist/lib/initControlPlane-C2zURVIS.js.map +1 -0
- package/dist/lib/{markdown-eeSTFnMA.js → markdown-C6EHl9zs.js} +3 -3
- package/dist/lib/{markdown-eeSTFnMA.js.map → markdown-C6EHl9zs.js.map} +1 -1
- package/dist/lib/{orchestrator-CJvoC2Uu.js → orchestrator-CACLa_Ro.js} +7 -6
- package/dist/lib/{orchestrator-CJvoC2Uu.js.map → orchestrator-CACLa_Ro.js.map} +1 -1
- package/dist/lib/{orchestrator-DCfomsn9.js → orchestrator-KRjx0eba.js} +6 -534
- package/dist/lib/{orchestrator-DCfomsn9.js.map → orchestrator-KRjx0eba.js.map} +1 -1
- package/dist/lib/parsePath-DwC_YZjw.js +16 -0
- package/dist/lib/parsePath-DwC_YZjw.js.map +1 -0
- package/dist/lib/{peerjs-Dp02bXcG.js → peerjs-DFWOBcXf.js} +4 -4
- package/dist/lib/{peerjs-Dp02bXcG.js.map → peerjs-DFWOBcXf.js.map} +1 -1
- package/dist/lib/{readGroupFileBytes-Bve-e5e_.js → readGroupFileBytes-C8oOe8Iu.js} +3 -16
- package/dist/lib/readGroupFileBytes-C8oOe8Iu.js.map +1 -0
- package/dist/lib/security/custom-element-security.d.ts +8 -0
- package/dist/lib/server/control-plane-types.d.ts +1 -1
- package/dist/lib/{shadow-claw-D8xINM3c.js → shadow-claw-6UHdiy4a.js} +5 -5
- package/dist/lib/{shadow-claw-D8xINM3c.js.map → shadow-claw-6UHdiy4a.js.map} +1 -1
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js → shadow-claw-a2ui-CpZUTzlh.js} +5 -4
- package/dist/lib/{shadow-claw-a2ui-7rJqG4Ke.js.map → shadow-claw-a2ui-CpZUTzlh.js.map} +1 -1
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js → shadow-claw-accounts-03oJ__XC.js} +2 -2
- package/dist/lib/{shadow-claw-accounts-CdruwN8A.js.map → shadow-claw-accounts-03oJ__XC.js.map} +1 -1
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js → shadow-claw-channel-config-DmfwJc6G.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-Bj0-Nk3t.js.map → shadow-claw-channel-config-DmfwJc6G.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js → shadow-claw-channels-1N6Jl-By.js} +3 -3
- package/dist/lib/{shadow-claw-channels-C_jI4KhP.js.map → shadow-claw-channels-1N6Jl-By.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-B16MspoP.js → shadow-claw-chat-BMoqQJRK.js} +10 -10
- package/dist/lib/{shadow-claw-chat-B16MspoP.js.map → shadow-claw-chat-BMoqQJRK.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js → shadow-claw-control-plane-UG0o7pCz.js} +6 -6
- package/dist/lib/{shadow-claw-control-plane-x8Ldid_h.js.map → shadow-claw-control-plane-UG0o7pCz.js.map} +1 -1
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js → shadow-claw-conversations-BwXep3iu.js} +4 -3
- package/dist/lib/{shadow-claw-conversations-Ct1kAYdB.js.map → shadow-claw-conversations-BwXep3iu.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-BfDHHgpA.js → shadow-claw-file-viewer-DUxzVxDT.js} +14 -10
- package/dist/lib/shadow-claw-file-viewer-DUxzVxDT.js.map +1 -0
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js → shadow-claw-files-DIREEnT2.js} +7 -7
- package/dist/lib/{shadow-claw-files-EYhOPYIH.js.map → shadow-claw-files-DIREEnT2.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js → shadow-claw-git-Bg1SWgsF.js} +4 -4
- package/dist/lib/{shadow-claw-git-tcSpT-P0.js.map → shadow-claw-git-Bg1SWgsF.js.map} +1 -1
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js → shadow-claw-integrations-Ds-CLPhj.js} +2 -2
- package/dist/lib/{shadow-claw-integrations-NbcCx_ZI.js.map → shadow-claw-integrations-Ds-CLPhj.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js → shadow-claw-llm-CazBrVpM.js} +3 -3
- package/dist/lib/{shadow-claw-llm-BazJdD5f.js.map → shadow-claw-llm-CazBrVpM.js.map} +1 -1
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js → shadow-claw-mcp-remote-CT5Q-54E.js} +2 -2
- package/dist/lib/{shadow-claw-mcp-remote-CYmhpg7a.js.map → shadow-claw-mcp-remote-CT5Q-54E.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js → shadow-claw-networking-Df85tCbO.js} +2 -2
- package/dist/lib/{shadow-claw-networking-DImPxe0H.js.map → shadow-claw-networking-Df85tCbO.js.map} +1 -1
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js → shadow-claw-page-header-Cti2fzf-.js} +2 -2
- package/dist/lib/{shadow-claw-page-header-CcHUfFUx.js.map → shadow-claw-page-header-Cti2fzf-.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-D54jF1Qq.js → shadow-claw-pages-BgPyIEZO.js} +15 -11
- package/dist/lib/shadow-claw-pages-BgPyIEZO.js.map +1 -0
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js → shadow-claw-peerjs-xkBXILed.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-B9-T8Nlx.js.map → shadow-claw-peerjs-xkBXILed.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js → shadow-claw-settings-CN89xRVU.js} +4 -4
- package/dist/lib/{shadow-claw-settings-Bt1VAe-1.js.map → shadow-claw-settings-CN89xRVU.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js → shadow-claw-storage-VnhcObCe.js} +3 -3
- package/dist/lib/{shadow-claw-storage-CFOjGf1d.js.map → shadow-claw-storage-VnhcObCe.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js → shadow-claw-task-server-jgEC2hHB.js} +2 -2
- package/dist/lib/{shadow-claw-task-server-BiKa2GOm.js.map → shadow-claw-task-server-jgEC2hHB.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js → shadow-claw-tasks-i_evIufy.js} +6 -6
- package/dist/lib/{shadow-claw-tasks-DDHd8W4d.js.map → shadow-claw-tasks-i_evIufy.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js → shadow-claw-terminal-CwScROlu.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-kB6_tWKz.js.map → shadow-claw-terminal-CwScROlu.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js → shadow-claw-tools-DD3fyXP4.js} +5 -4
- package/dist/lib/{shadow-claw-tools-DxXIm0ku.js.map → shadow-claw-tools-DD3fyXP4.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js → shadow-claw-webvm-DbJq4I3-.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-Dc8oo7ao.js.map → shadow-claw-webvm-DbJq4I3-.js.map} +1 -1
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js → syncWebMcpRegistration-DNLnQ-jz.js} +3 -2
- package/dist/lib/{syncWebMcpRegistration-jyRGbC9w.js.map → syncWebMcpRegistration-DNLnQ-jz.js.map} +1 -1
- package/dist/lib/tools-DwwB9RBa.js +546 -0
- package/dist/lib/tools-DwwB9RBa.js.map +1 -0
- package/dist/lib/{trusted-types-CkLDpLiZ.js → trusted-types-Dx0RZlrd.js} +19 -8
- package/dist/lib/trusted-types-Dx0RZlrd.js.map +1 -0
- package/dist/lib/utils/index.js +1 -1
- package/dist/lib/utils/utils.js +1 -1
- package/dist/public/.well-known/agent-skills/index.json +14 -0
- package/dist/public/AGENTS.md +5 -3
- package/dist/public/README.md +5 -3
- package/dist/public/agent.worker.js +1 -1
- package/dist/public/{configurePeerJs-COcwE0E7.js → configurePeerJs-BqgeBjP7.js} +1 -1
- package/dist/public/custom-element-security-FU04Cq05.js +1 -0
- package/dist/public/dist-B9m1QM9v.js +58 -0
- package/dist/public/docs/README.md +31 -31
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/a2ui.md +11 -1
- package/dist/public/docs/subsystems/cli.md +29 -0
- package/dist/public/docs/subsystems/control-plane.md +8 -2
- package/dist/public/docs/subsystems/custom-element-security.md +19 -9
- package/dist/public/docs/subsystems/mcp-server.md +39 -15
- package/dist/public/docs/subsystems/skills.md +30 -0
- package/dist/public/docs/subsystems/webmcp.md +9 -0
- package/dist/public/downloadGroupFile-D8KRJGGL.js +1 -0
- package/dist/public/e2e/README.md +7 -4
- package/dist/public/{e2e-bridge-CHARpbpW.js → e2e-bridge-Ceqr5kGc.js} +1 -1
- package/dist/public/effect-BJCrpFdp.js +1 -0
- package/dist/public/file-viewer-C-6Jc_tb.js +1 -0
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/initControlPlane-BN9cWyO0.js +1 -0
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-1I3O6Mmd.js → markdown-C3M4OW_A.js} +1 -1
- package/dist/public/{orchestrator-Cl95w2vb.js → orchestrator-CMbCTjBf.js} +7 -7
- package/dist/public/{orchestrator-D-xlXG92.js → orchestrator-DHjdLtqa.js} +663 -663
- package/dist/public/peerjs-DUCKH-m9.js +1 -0
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-CaoKmIIB.js → shadow-claw-EAt2ml5Y.js} +3 -3
- package/dist/public/{shadow-claw-accounts-CJvYC4li.js → shadow-claw-accounts-xUOLBsQZ.js} +1 -1
- package/dist/public/{shadow-claw-channels-3K62sU5f.js → shadow-claw-channels-XsfuWFF8.js} +4 -4
- package/dist/public/{shadow-claw-chat-Cu2keU2I.js → shadow-claw-chat-CbSaOZ4q.js} +7 -7
- package/dist/public/{shadow-claw-control-plane-D1OwlQI_.js → shadow-claw-control-plane-DHvN3Ukg.js} +2 -2
- package/dist/public/{shadow-claw-conversations-DDtg3BEZ.js → shadow-claw-conversations-BWs5VVfs.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-BR__1z9S.js → shadow-claw-file-viewer-BWsoW5QS.js} +5 -4
- package/dist/public/{shadow-claw-files-BJSyZ-Bl.js → shadow-claw-files-CG7MqWiV.js} +8 -8
- package/dist/public/{shadow-claw-git-MAjhXRLc.js → shadow-claw-git-DBRPZNav.js} +6 -6
- package/dist/public/{shadow-claw-integrations-82UoFuDR.js → shadow-claw-integrations-BBVnO0Fc.js} +8 -8
- package/dist/public/{shadow-claw-llm-R8j0lwPP.js → shadow-claw-llm-DMvLgKXo.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-BzZoz4LX.js → shadow-claw-mcp-remote-Bkx0gsmF.js} +2 -2
- package/dist/public/{shadow-claw-networking-DLEd9cNm.js → shadow-claw-networking-Dd8Rw31D.js} +2 -2
- package/dist/public/{shadow-claw-notifications-1vwR7TbT.js → shadow-claw-notifications-Dbevj-lq.js} +2 -2
- package/dist/public/{shadow-claw-page-header-DMKbkv3s.js → shadow-claw-page-header-BccSHItx.js} +1 -1
- package/dist/public/{shadow-claw-pages-CjyeIV__.js → shadow-claw-pages-3_lQAv0t.js} +4 -3
- package/dist/public/{shadow-claw-settings-BfbT4dBU.js → shadow-claw-settings-t8TIlgrM.js} +5 -5
- package/dist/public/{shadow-claw-storage-C-mWQQML.js → shadow-claw-storage-CfEXoSIE.js} +2 -2
- package/dist/public/{shadow-claw-task-server-D3lHp0Nc.js → shadow-claw-task-server-DxQSsgRZ.js} +2 -2
- package/dist/public/{shadow-claw-tasks-DVVWEm-c.js → shadow-claw-tasks-DNpicQwz.js} +14 -14
- package/dist/public/{shadow-claw-terminal-BwuTRp-O.js → shadow-claw-terminal-DpY6zU_r.js} +1 -1
- package/dist/public/{shadow-claw-toast-ClCwgzFG.js → shadow-claw-toast-BkrQIP-3.js} +1 -1
- package/dist/public/{shadow-claw-tools-zFLk2nbV.js → shadow-claw-tools-WSwWohnk.js} +2 -2
- package/dist/public/{shadow-claw-webvm-qR9X7fg7.js → shadow-claw-webvm-CP6oFjg5.js} +2 -2
- package/dist/public/syncWebMcpRegistration-DOn9gLnD.js +1 -0
- package/dist/public/toast-BXgUfbyh.js +1 -0
- package/dist/public/tools-DvckJZG2.js +1 -0
- package/dist/server.js +188 -188
- package/package.json +3 -1
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.stories.ts +1380 -93
- package/src/components/shadow-claw-a2ui/shadow-claw-a2ui.ts +11 -6
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +8 -1
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +13 -5
- package/src/components/shadow-claw-pages/utils/buildHtmlPageSrcdoc.ts +13 -5
- package/src/core/control-plane-client.ts +15 -0
- package/src/core/utils/initControlPlane.test.ts +187 -0
- package/src/core/utils/initControlPlane.ts +194 -20
- package/src/security/custom-element-security.test.ts +51 -0
- package/src/security/custom-element-security.ts +33 -15
- package/src/server/control-plane-types.ts +1 -0
- package/src/server/control-plane.test.ts +69 -0
- package/src/server/control-plane.ts +45 -3
- package/src/server/mcp/tools/built-in-tools.ts +80 -5
- package/src/server/mcp/tools/client-tool-relay.ts +183 -26
- package/src/server/mcp/tools/mcp-tools.test.ts +140 -0
- package/src/server/middleware/static-files.test.ts +142 -0
- package/src/server/middleware/static-files.ts +66 -8
- package/src/testing/jest-setup.ts +1 -0
- package/src/worker/utils/handleMessage.ts +4 -1
- package/dist/lib/initControlPlane-CsO4UR6_.js.map +0 -1
- package/dist/lib/readGroupFileBytes-Bve-e5e_.js.map +0 -1
- package/dist/lib/shadow-claw-file-viewer-BfDHHgpA.js.map +0 -1
- package/dist/lib/shadow-claw-pages-D54jF1Qq.js.map +0 -1
- package/dist/lib/trusted-types-CkLDpLiZ.js.map +0 -1
- package/dist/public/custom-element-security-NCqQ5lmC.js +0 -1
- package/dist/public/downloadGroupFile-G0BZQgQg.js +0 -1
- package/dist/public/effect-rHYr4nfu.js +0 -1
- package/dist/public/file-viewer-8PdCURDD.js +0 -1
- package/dist/public/initControlPlane-CcGXax0A.js +0 -1
- package/dist/public/peerjs-BZdxWQFv.js +0 -1
- package/dist/public/syncWebMcpRegistration-DD42YsYV.js +0 -1
- package/dist/public/toast-HAq8XWFj.js +0 -58
package/bin/commands/mcp.mjs
CHANGED
|
@@ -71,6 +71,22 @@ export const CLI_BUILTIN_TOOLS = [
|
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "shadowclaw_set_active_client",
|
|
76
|
+
description:
|
|
77
|
+
"Set the active default connected client used when no clientId is explicitly provided in tool calls.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
clientId: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description:
|
|
84
|
+
"Target client ID (or index '0', '1', prefix, or device label) to make active.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ["clientId"],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
74
90
|
{
|
|
75
91
|
name: "shadowclaw_manage_backup",
|
|
76
92
|
description:
|
|
@@ -95,6 +111,7 @@ export function createCliMcpEngine(options = {}) {
|
|
|
95
111
|
const client = options.client || new CliControlClient(options);
|
|
96
112
|
const relayClientTools = options.relayClientTools !== false;
|
|
97
113
|
const targetClientId = options.targetClientId || options.clientTarget;
|
|
114
|
+
let activeClientId = targetClientId || "";
|
|
98
115
|
|
|
99
116
|
const serverInfo = {
|
|
100
117
|
name: "shadow-claw",
|
|
@@ -107,47 +124,138 @@ export function createCliMcpEngine(options = {}) {
|
|
|
107
124
|
};
|
|
108
125
|
|
|
109
126
|
async function resolveTargetId(requestedId) {
|
|
110
|
-
|
|
111
|
-
return requestedId.trim();
|
|
112
|
-
}
|
|
113
|
-
if (targetClientId) {
|
|
114
|
-
return targetClientId;
|
|
115
|
-
}
|
|
127
|
+
const candidate = requestedId || activeClientId || targetClientId;
|
|
116
128
|
try {
|
|
117
129
|
const clients = await client.listClients();
|
|
118
130
|
if (Array.isArray(clients) && clients.length > 0) {
|
|
131
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
132
|
+
const trimmed = candidate.trim();
|
|
133
|
+
const exact = clients.find((c) => (c.clientId || c.id) === trimmed);
|
|
134
|
+
if (exact) return exact.clientId || exact.id;
|
|
135
|
+
|
|
136
|
+
const idx = parseInt(trimmed, 10);
|
|
137
|
+
if (!isNaN(idx) && idx >= 0 && idx < clients.length) {
|
|
138
|
+
return clients[idx].clientId || clients[idx].id;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const prefix = clients.find(
|
|
142
|
+
(c) =>
|
|
143
|
+
(c.clientId || c.id || "").startsWith(trimmed) ||
|
|
144
|
+
(c.clientId || "").replace(/^client-/, "").startsWith(trimmed),
|
|
145
|
+
);
|
|
146
|
+
if (prefix) return prefix.clientId || prefix.id;
|
|
147
|
+
|
|
148
|
+
const byLabel = clients.find((c) =>
|
|
149
|
+
c.deviceLabel?.toLowerCase().includes(trimmed.toLowerCase()),
|
|
150
|
+
);
|
|
151
|
+
if (byLabel) return byLabel.clientId || byLabel.id;
|
|
152
|
+
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
|
|
119
156
|
return clients[0].clientId || clients[0].id || "";
|
|
120
157
|
}
|
|
121
158
|
} catch (_) {}
|
|
122
|
-
return "";
|
|
159
|
+
return typeof candidate === "string" ? candidate.trim() : "";
|
|
123
160
|
}
|
|
124
161
|
|
|
162
|
+
const toolSupportingClientsMap = new Map();
|
|
163
|
+
const toolDefMap = new Map();
|
|
164
|
+
|
|
125
165
|
async function getRelayedTools() {
|
|
126
166
|
if (!relayClientTools) {
|
|
127
167
|
return [];
|
|
128
168
|
}
|
|
129
|
-
|
|
130
|
-
if (!clientId) {
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
169
|
+
let clients = [];
|
|
133
170
|
try {
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
res &&
|
|
137
|
-
res.success &&
|
|
138
|
-
res.data?.tools &&
|
|
139
|
-
Array.isArray(res.data.tools)
|
|
140
|
-
) {
|
|
141
|
-
return res.data.tools.map((t) => ({
|
|
142
|
-
name: t.name,
|
|
143
|
-
description:
|
|
144
|
-
t.description || `Relayed tool '${t.name}' from connected client.`,
|
|
145
|
-
inputSchema: t.inputSchema || { type: "object", properties: {} },
|
|
146
|
-
annotations: t.annotations,
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
171
|
+
clients = await client.listClients();
|
|
149
172
|
} catch (_) {}
|
|
150
|
-
|
|
173
|
+
|
|
174
|
+
if (!Array.isArray(clients) || clients.length === 0) {
|
|
175
|
+
toolSupportingClientsMap.clear();
|
|
176
|
+
toolDefMap.clear();
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
toolSupportingClientsMap.clear();
|
|
181
|
+
toolDefMap.clear();
|
|
182
|
+
|
|
183
|
+
for (const c of clients) {
|
|
184
|
+
const cid = c.clientId || c.id;
|
|
185
|
+
if (!cid) continue;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const res = await client.sendCommand(cid, "list-tools", {});
|
|
189
|
+
if (
|
|
190
|
+
res &&
|
|
191
|
+
res.success &&
|
|
192
|
+
res.data?.tools &&
|
|
193
|
+
Array.isArray(res.data.tools)
|
|
194
|
+
) {
|
|
195
|
+
for (const t of res.data.tools) {
|
|
196
|
+
if (!toolDefMap.has(t.name)) {
|
|
197
|
+
toolDefMap.set(t.name, t);
|
|
198
|
+
toolSupportingClientsMap.set(t.name, []);
|
|
199
|
+
}
|
|
200
|
+
toolSupportingClientsMap.get(t.name).push(c);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (_) {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const activeTargetId = await resolveTargetId();
|
|
207
|
+
const relayedTools = [];
|
|
208
|
+
|
|
209
|
+
for (const [toolName, t] of toolDefMap.entries()) {
|
|
210
|
+
const supportingClients = toolSupportingClientsMap.get(toolName) || [];
|
|
211
|
+
const supportingClientIds = supportingClients
|
|
212
|
+
.map((cl) => cl.clientId || cl.id)
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
|
|
215
|
+
const preferredClient =
|
|
216
|
+
supportingClients.find(
|
|
217
|
+
(cl) => (cl.clientId || cl.id) === activeTargetId,
|
|
218
|
+
) || supportingClients[0];
|
|
219
|
+
const toolDefaultId =
|
|
220
|
+
preferredClient?.clientId || preferredClient?.id || "";
|
|
221
|
+
const toolDefaultLabel = preferredClient?.deviceLabel || "Client";
|
|
222
|
+
|
|
223
|
+
const existingSchema =
|
|
224
|
+
t.inputSchema && typeof t.inputSchema === "object"
|
|
225
|
+
? t.inputSchema
|
|
226
|
+
: { type: "object", properties: {} };
|
|
227
|
+
|
|
228
|
+
const properties = {
|
|
229
|
+
...(existingSchema.properties || {}),
|
|
230
|
+
clientId: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: `Target ShadowClaw client ID (optional; defaults to ${toolDefaultId} [${toolDefaultLabel}]). Available on: ${supportingClients.map((cl) => `${cl.clientId || cl.id} (${cl.deviceLabel || "Client"})`).join(", ")}`,
|
|
233
|
+
enum: supportingClientIds,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const clientNote =
|
|
238
|
+
clients.length > 1
|
|
239
|
+
? supportingClients.length > 1
|
|
240
|
+
? ` [Default: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
|
|
241
|
+
: ` [Client: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
|
|
242
|
+
: "";
|
|
243
|
+
|
|
244
|
+
relayedTools.push({
|
|
245
|
+
name: t.name,
|
|
246
|
+
description:
|
|
247
|
+
(t.description || `Relayed tool '${t.name}' from connected client.`) +
|
|
248
|
+
clientNote,
|
|
249
|
+
inputSchema: {
|
|
250
|
+
...existingSchema,
|
|
251
|
+
type: "object",
|
|
252
|
+
properties,
|
|
253
|
+
},
|
|
254
|
+
annotations: t.annotations,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return relayedTools;
|
|
151
259
|
}
|
|
152
260
|
|
|
153
261
|
async function handleMessage(request, headers = {}) {
|
|
@@ -260,6 +368,48 @@ export function createCliMcpEngine(options = {}) {
|
|
|
260
368
|
};
|
|
261
369
|
}
|
|
262
370
|
|
|
371
|
+
if (toolName === "shadowclaw_set_active_client") {
|
|
372
|
+
const targetId = await resolveTargetId(args.clientId);
|
|
373
|
+
if (!targetId) {
|
|
374
|
+
return {
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
id: reqId,
|
|
377
|
+
result: {
|
|
378
|
+
resultType: "complete",
|
|
379
|
+
isError: true,
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: "text",
|
|
383
|
+
text: `Error: Client '${args.clientId}' not found among connected clients.`,
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
activeClientId = targetId;
|
|
391
|
+
console.error(
|
|
392
|
+
`[ShadowClaw MCP] Active default client set to: ${activeClientId}`,
|
|
393
|
+
);
|
|
394
|
+
return {
|
|
395
|
+
jsonrpc: "2.0",
|
|
396
|
+
id: reqId,
|
|
397
|
+
result: {
|
|
398
|
+
resultType: "complete",
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: "text",
|
|
402
|
+
text: `Active default client set to: ${activeClientId}`,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
_meta: {
|
|
406
|
+
activeClientId,
|
|
407
|
+
"io.modelcontextprotocol/serverInfo": serverInfo,
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
263
413
|
if (toolName === "shadowclaw_send_message") {
|
|
264
414
|
const targetId = await resolveTargetId(args.clientId);
|
|
265
415
|
if (!targetId) {
|
|
@@ -391,26 +541,12 @@ export function createCliMcpEngine(options = {}) {
|
|
|
391
541
|
};
|
|
392
542
|
}
|
|
393
543
|
|
|
394
|
-
// Relayed interactive ask_user tool
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
result: {
|
|
401
|
-
resultType: "input_required",
|
|
402
|
-
inputRequests: [
|
|
403
|
-
{
|
|
404
|
-
id: "response",
|
|
405
|
-
type: "prompt",
|
|
406
|
-
message:
|
|
407
|
-
args.question || args.prompt || "User input requested",
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
}
|
|
544
|
+
// Relayed interactive ask_user tool with MRTR response fulfillment if provided
|
|
545
|
+
if (
|
|
546
|
+
toolName === "ask_user" &&
|
|
547
|
+
inputResponses &&
|
|
548
|
+
inputResponses["response"]
|
|
549
|
+
) {
|
|
414
550
|
return {
|
|
415
551
|
jsonrpc: "2.0",
|
|
416
552
|
id: reqId,
|
|
@@ -425,7 +561,29 @@ export function createCliMcpEngine(options = {}) {
|
|
|
425
561
|
}
|
|
426
562
|
|
|
427
563
|
// Relayed client tools (invoke-tool)
|
|
428
|
-
|
|
564
|
+
if (toolDefMap.size === 0) {
|
|
565
|
+
await getRelayedTools();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const supportingClients = toolSupportingClientsMap.get(toolName) || [];
|
|
569
|
+
const supportingClientIds = supportingClients
|
|
570
|
+
.map((cl) => cl.clientId || cl.id)
|
|
571
|
+
.filter(Boolean);
|
|
572
|
+
|
|
573
|
+
let targetId = "";
|
|
574
|
+
if (args.clientId) {
|
|
575
|
+
targetId = await resolveTargetId(args.clientId);
|
|
576
|
+
} else {
|
|
577
|
+
const activeSessionId = await resolveTargetId();
|
|
578
|
+
if (supportingClientIds.includes(activeSessionId)) {
|
|
579
|
+
targetId = activeSessionId;
|
|
580
|
+
} else if (supportingClientIds.length > 0) {
|
|
581
|
+
targetId = supportingClientIds[0];
|
|
582
|
+
} else {
|
|
583
|
+
targetId = activeSessionId;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
429
587
|
if (!targetId) {
|
|
430
588
|
return {
|
|
431
589
|
jsonrpc: "2.0",
|
|
@@ -441,11 +599,51 @@ export function createCliMcpEngine(options = {}) {
|
|
|
441
599
|
};
|
|
442
600
|
}
|
|
443
601
|
|
|
602
|
+
if (
|
|
603
|
+
supportingClientIds.length > 0 &&
|
|
604
|
+
!supportingClientIds.includes(targetId)
|
|
605
|
+
) {
|
|
606
|
+
return {
|
|
607
|
+
jsonrpc: "2.0",
|
|
608
|
+
id: reqId,
|
|
609
|
+
result: {
|
|
610
|
+
resultType: "complete",
|
|
611
|
+
isError: true,
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: "text",
|
|
615
|
+
text: `Error: Tool '${toolName}' is not enabled or available on client '${targetId}'. (Available on: ${supportingClientIds.join(", ")})`,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
_meta: {
|
|
619
|
+
clientId: targetId,
|
|
620
|
+
"io.modelcontextprotocol/serverInfo": serverInfo,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
444
626
|
try {
|
|
445
|
-
|
|
446
|
-
toolName
|
|
447
|
-
|
|
448
|
-
}
|
|
627
|
+
console.error(
|
|
628
|
+
`[ShadowClaw MCP] Executing '${toolName}' on client: ${targetId}`,
|
|
629
|
+
);
|
|
630
|
+
const toolArgs = { ...args };
|
|
631
|
+
delete toolArgs.clientId;
|
|
632
|
+
const isInteractive = toolName === "ask_user";
|
|
633
|
+
const res = isInteractive
|
|
634
|
+
? await client.sendCommand(
|
|
635
|
+
targetId,
|
|
636
|
+
"invoke-tool",
|
|
637
|
+
{
|
|
638
|
+
toolName,
|
|
639
|
+
input: toolArgs,
|
|
640
|
+
},
|
|
641
|
+
300000,
|
|
642
|
+
)
|
|
643
|
+
: await client.sendCommand(targetId, "invoke-tool", {
|
|
644
|
+
toolName,
|
|
645
|
+
input: toolArgs,
|
|
646
|
+
});
|
|
449
647
|
|
|
450
648
|
const rawResult =
|
|
451
649
|
res.data?.result !== undefined ? res.data.result : res.data;
|
|
@@ -454,14 +652,31 @@ export function createCliMcpEngine(options = {}) {
|
|
|
454
652
|
? rawResult
|
|
455
653
|
: JSON.stringify(rawResult, null, 2);
|
|
456
654
|
|
|
655
|
+
const errorMessage =
|
|
656
|
+
res.error ||
|
|
657
|
+
(res.data && res.data.error) ||
|
|
658
|
+
"Unknown tool execution error";
|
|
659
|
+
|
|
457
660
|
return {
|
|
458
661
|
jsonrpc: "2.0",
|
|
459
662
|
id: reqId,
|
|
460
663
|
result: {
|
|
461
664
|
resultType: "complete",
|
|
462
665
|
isError: !res.success,
|
|
463
|
-
content: [
|
|
464
|
-
|
|
666
|
+
content: [
|
|
667
|
+
{
|
|
668
|
+
type: "text",
|
|
669
|
+
text: res.success
|
|
670
|
+
? textOutput || ""
|
|
671
|
+
: textOutput && textOutput !== "null" && textOutput !== "{}"
|
|
672
|
+
? `${textOutput}\n${errorMessage}`
|
|
673
|
+
: `Tool execution error: ${errorMessage}`,
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
_meta: {
|
|
677
|
+
clientId: targetId,
|
|
678
|
+
"io.modelcontextprotocol/serverInfo": serverInfo,
|
|
679
|
+
},
|
|
465
680
|
},
|
|
466
681
|
};
|
|
467
682
|
} catch (err) {
|
|
@@ -472,7 +687,10 @@ export function createCliMcpEngine(options = {}) {
|
|
|
472
687
|
resultType: "complete",
|
|
473
688
|
isError: true,
|
|
474
689
|
content: [{ type: "text", text: `Tool error: ${err.message}` }],
|
|
475
|
-
_meta: {
|
|
690
|
+
_meta: {
|
|
691
|
+
clientId: targetId,
|
|
692
|
+
"io.modelcontextprotocol/serverInfo": serverInfo,
|
|
693
|
+
},
|
|
476
694
|
},
|
|
477
695
|
};
|
|
478
696
|
}
|
|
@@ -511,6 +729,10 @@ export function createCliMcpEngine(options = {}) {
|
|
|
511
729
|
|
|
512
730
|
return {
|
|
513
731
|
handleMessage,
|
|
732
|
+
getActiveClientId: () => activeClientId,
|
|
733
|
+
setActiveClientId: (id) => {
|
|
734
|
+
activeClientId = id;
|
|
735
|
+
},
|
|
514
736
|
};
|
|
515
737
|
}
|
|
516
738
|
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
it,
|
|
4
|
+
expect,
|
|
5
|
+
jest,
|
|
6
|
+
beforeEach,
|
|
7
|
+
afterEach,
|
|
8
|
+
} from "@jest/globals";
|
|
2
9
|
import { createCliMcpEngine } from "./mcp.mjs";
|
|
3
10
|
|
|
4
11
|
describe("CLI MCP Engine (createCliMcpEngine)", () => {
|
|
12
|
+
let errorSpy;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
errorSpy?.mockRestore();
|
|
20
|
+
});
|
|
21
|
+
|
|
5
22
|
it("processes server/discover request", async () => {
|
|
6
23
|
const mockClient = {
|
|
7
24
|
listClients: jest
|
|
@@ -97,4 +114,261 @@ describe("CLI MCP Engine (createCliMcpEngine)", () => {
|
|
|
97
114
|
groupId: undefined,
|
|
98
115
|
});
|
|
99
116
|
});
|
|
117
|
+
|
|
118
|
+
it("processes tools/call for relayed client tool (e.g. list_files)", async () => {
|
|
119
|
+
const mockClient = {
|
|
120
|
+
listClients: jest
|
|
121
|
+
.fn()
|
|
122
|
+
.mockResolvedValue([{ clientId: "c1", deviceLabel: "Browser Test" }]),
|
|
123
|
+
sendCommand: jest
|
|
124
|
+
.fn()
|
|
125
|
+
.mockImplementation(async (clientId, action, payload) => {
|
|
126
|
+
if (action === "invoke-tool" && payload.toolName === "list_files") {
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: {
|
|
130
|
+
result: ".agents/ MEMORY.md index.html -/",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { success: false, error: "Not handled" };
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
139
|
+
const res = await engine.handleMessage({
|
|
140
|
+
jsonrpc: "2.0",
|
|
141
|
+
id: 4,
|
|
142
|
+
method: "tools/call",
|
|
143
|
+
params: {
|
|
144
|
+
name: "list_files",
|
|
145
|
+
arguments: {},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(res).toBeDefined();
|
|
150
|
+
expect(res.id).toBe(4);
|
|
151
|
+
expect(res.result.resultType).toBe("complete");
|
|
152
|
+
expect(res.result.isError).toBe(false);
|
|
153
|
+
expect(res.result.content[0].text).toBe(".agents/ MEMORY.md index.html -/");
|
|
154
|
+
expect(mockClient.sendCommand).toHaveBeenCalledWith("c1", "invoke-tool", {
|
|
155
|
+
toolName: "list_files",
|
|
156
|
+
input: {},
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles tools/call relayed client tool failure with informative error content", async () => {
|
|
161
|
+
const mockClient = {
|
|
162
|
+
listClients: jest
|
|
163
|
+
.fn()
|
|
164
|
+
.mockResolvedValue([{ clientId: "c1", deviceLabel: "Browser Test" }]),
|
|
165
|
+
sendCommand: jest.fn().mockImplementation(async () => {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: "Permission denied reading directory",
|
|
169
|
+
};
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
174
|
+
const res = await engine.handleMessage({
|
|
175
|
+
jsonrpc: "2.0",
|
|
176
|
+
id: 5,
|
|
177
|
+
method: "tools/call",
|
|
178
|
+
params: {
|
|
179
|
+
name: "list_files",
|
|
180
|
+
arguments: { path: "restricted" },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(res).toBeDefined();
|
|
185
|
+
expect(res.id).toBe(5);
|
|
186
|
+
expect(res.result.resultType).toBe("complete");
|
|
187
|
+
expect(res.result.isError).toBe(true);
|
|
188
|
+
expect(res.result.content[0].text).toContain(
|
|
189
|
+
"Permission denied reading directory",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("handles multi-client discovery and explicit clientId targeting", async () => {
|
|
194
|
+
const mockClient = {
|
|
195
|
+
listClients: jest.fn().mockResolvedValue([
|
|
196
|
+
{ clientId: "client-alpha", deviceLabel: "Linux Desktop" },
|
|
197
|
+
{ clientId: "client-beta", deviceLabel: "Android Phone" },
|
|
198
|
+
]),
|
|
199
|
+
sendCommand: jest
|
|
200
|
+
.fn()
|
|
201
|
+
.mockImplementation(async (clientId, action, payload) => {
|
|
202
|
+
if (action === "list-tools") {
|
|
203
|
+
const tools = [
|
|
204
|
+
{
|
|
205
|
+
name: "list_files",
|
|
206
|
+
description: "List workspace files.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: { path: { type: "string" } },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
if (clientId === "client-alpha") {
|
|
214
|
+
tools.push({
|
|
215
|
+
name: "ask_user",
|
|
216
|
+
description: "Ask user for confirmation.",
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: { question: { type: "string" } },
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
data: { tools },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (action === "invoke-tool") {
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
data: {
|
|
232
|
+
result: `Files from ${clientId}`,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return { success: false };
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const engine = createCliMcpEngine({ client: mockClient });
|
|
241
|
+
|
|
242
|
+
// 1. tools/list should augment schema with clientId enum and default client info
|
|
243
|
+
const listRes = await engine.handleMessage({
|
|
244
|
+
jsonrpc: "2.0",
|
|
245
|
+
id: 10,
|
|
246
|
+
method: "tools/list",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const listFilesTool = listRes.result.tools.find(
|
|
250
|
+
(t) => t.name === "list_files",
|
|
251
|
+
);
|
|
252
|
+
expect(listFilesTool).toBeDefined();
|
|
253
|
+
expect(listFilesTool.description).toContain("[Default: Linux Desktop");
|
|
254
|
+
expect(listFilesTool.inputSchema.properties.clientId).toBeDefined();
|
|
255
|
+
expect(listFilesTool.inputSchema.properties.clientId.enum).toEqual([
|
|
256
|
+
"client-alpha",
|
|
257
|
+
"client-beta",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
// ask_user should ONLY be available on client-alpha
|
|
261
|
+
const askUserTool = listRes.result.tools.find((t) => t.name === "ask_user");
|
|
262
|
+
expect(askUserTool).toBeDefined();
|
|
263
|
+
expect(askUserTool.description).toContain("[Client: Linux Desktop");
|
|
264
|
+
expect(askUserTool.inputSchema.properties.clientId.enum).toEqual([
|
|
265
|
+
"client-alpha",
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
// 2. tools/call targeting client-beta explicitly
|
|
269
|
+
const callResBeta = await engine.handleMessage({
|
|
270
|
+
jsonrpc: "2.0",
|
|
271
|
+
id: 11,
|
|
272
|
+
method: "tools/call",
|
|
273
|
+
params: {
|
|
274
|
+
name: "list_files",
|
|
275
|
+
arguments: { clientId: "client-beta" },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(mockClient.sendCommand).toHaveBeenCalledWith(
|
|
280
|
+
"client-beta",
|
|
281
|
+
"invoke-tool",
|
|
282
|
+
{
|
|
283
|
+
toolName: "list_files",
|
|
284
|
+
input: {},
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
expect(callResBeta.result.content[0].text).toBe("Files from client-beta");
|
|
288
|
+
|
|
289
|
+
// 3. Switch active default client to client-beta
|
|
290
|
+
const setActiveRes = await engine.handleMessage({
|
|
291
|
+
jsonrpc: "2.0",
|
|
292
|
+
id: 12,
|
|
293
|
+
method: "tools/call",
|
|
294
|
+
params: {
|
|
295
|
+
name: "shadowclaw_set_active_client",
|
|
296
|
+
arguments: { clientId: "client-beta" },
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(setActiveRes.result.content[0].text).toContain(
|
|
301
|
+
"Active default client set to: client-beta",
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// 4. tools/call without clientId now defaults to client-beta for list_files
|
|
305
|
+
const callResDefault = await engine.handleMessage({
|
|
306
|
+
jsonrpc: "2.0",
|
|
307
|
+
id: 13,
|
|
308
|
+
method: "tools/call",
|
|
309
|
+
params: {
|
|
310
|
+
name: "list_files",
|
|
311
|
+
arguments: {},
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(callResDefault.result.content[0].text).toBe(
|
|
316
|
+
"Files from client-beta",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// 5. Attempting to call ask_user on client-beta should be REJECTED because client-beta does not support it
|
|
320
|
+
const unsupportedRes = await engine.handleMessage({
|
|
321
|
+
jsonrpc: "2.0",
|
|
322
|
+
id: 14,
|
|
323
|
+
method: "tools/call",
|
|
324
|
+
params: {
|
|
325
|
+
name: "ask_user",
|
|
326
|
+
arguments: { clientId: "client-beta", question: "Prompt client beta?" },
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(unsupportedRes.result.isError).toBe(true);
|
|
331
|
+
expect(unsupportedRes.result.content[0].text).toContain(
|
|
332
|
+
"Tool 'ask_user' is not enabled or available on client 'client-beta'",
|
|
333
|
+
);
|
|
334
|
+
expect(unsupportedRes.result.content[0].text).toContain(
|
|
335
|
+
"Available on: client-alpha",
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// 6. Calling ask_user without clientId should automatically default to client-alpha
|
|
339
|
+
// (the only supporting client), even though active default is client-beta!
|
|
340
|
+
const askUserRes = await engine.handleMessage({
|
|
341
|
+
jsonrpc: "2.0",
|
|
342
|
+
id: 15,
|
|
343
|
+
method: "tools/call",
|
|
344
|
+
params: {
|
|
345
|
+
name: "ask_user",
|
|
346
|
+
arguments: { question: "Are you ready?" },
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(mockClient.sendCommand).toHaveBeenCalledWith(
|
|
351
|
+
"client-alpha",
|
|
352
|
+
"invoke-tool",
|
|
353
|
+
{
|
|
354
|
+
toolName: "ask_user",
|
|
355
|
+
input: { question: "Are you ready?" },
|
|
356
|
+
},
|
|
357
|
+
300000,
|
|
358
|
+
);
|
|
359
|
+
expect(askUserRes.result.content[0].text).toBe("Files from client-alpha");
|
|
360
|
+
|
|
361
|
+
// 7. tools/call for ask_user with inputResponses returns fulfilled response
|
|
362
|
+
const fulfilledRes = await engine.handleMessage({
|
|
363
|
+
jsonrpc: "2.0",
|
|
364
|
+
id: 16,
|
|
365
|
+
method: "tools/call",
|
|
366
|
+
params: {
|
|
367
|
+
name: "ask_user",
|
|
368
|
+
arguments: { question: "Are you ready?" },
|
|
369
|
+
inputResponses: { response: "Yes, ready to go!" },
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
expect(fulfilledRes.result.content[0].text).toBe("Yes, ready to go!");
|
|
373
|
+
});
|
|
100
374
|
});
|