shadow-claw 1.28.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +96 -278
  4. package/bin/cli.mjs +4 -0
  5. package/bin/commands/mcp.mjs +409 -125
  6. package/bin/commands/mcp.test.mjs +151 -18
  7. package/bin/utils/control-client.mjs +338 -103
  8. package/bin/utils/control-client.test.mjs +14 -0
  9. package/dist/electron/main.cjs +179 -179
  10. package/dist/lib/components/index.js +21 -21
  11. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  12. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  13. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  14. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  15. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  16. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  17. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  18. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  19. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  20. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  21. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  22. package/dist/lib/components/shadow-claw-channels.js +1 -1
  23. package/dist/lib/components/shadow-claw-chat.js +1 -1
  24. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  25. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  26. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  27. package/dist/lib/components/shadow-claw-files.js +1 -1
  28. package/dist/lib/components/shadow-claw-git.js +1 -1
  29. package/dist/lib/components/shadow-claw-llm.js +1 -1
  30. package/dist/lib/components/shadow-claw-networking.js +1 -1
  31. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  32. package/dist/lib/components/shadow-claw-pages.js +1 -1
  33. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  34. package/dist/lib/components/shadow-claw-settings.js +1 -1
  35. package/dist/lib/components/shadow-claw-storage.js +1 -1
  36. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  37. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  38. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  39. package/dist/lib/components/shadow-claw-tools.js +1 -1
  40. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  41. package/dist/lib/components/shadow-claw.js +1 -1
  42. package/dist/lib/{configurePeerJs-D8Ph0-A9.js → configurePeerJs-DG4hliKp.js} +2 -2
  43. package/dist/lib/{configurePeerJs-D8Ph0-A9.js.map → configurePeerJs-DG4hliKp.js.map} +1 -1
  44. package/dist/lib/index.js +21 -21
  45. package/dist/lib/{initControlPlane-BIrWmpzc.js → initControlPlane-DnpAzphg.js} +8 -8
  46. package/dist/lib/{initControlPlane-BIrWmpzc.js.map → initControlPlane-DnpAzphg.js.map} +1 -1
  47. package/dist/lib/{markdown-DEUoM1c6.js → markdown-DgdSt2GD.js} +2 -2
  48. package/dist/lib/{markdown-DEUoM1c6.js.map → markdown-DgdSt2GD.js.map} +1 -1
  49. package/dist/lib/{orchestrator-CFCW-l1O.js → orchestrator-CjEau_RG.js} +5 -5
  50. package/dist/lib/{orchestrator-CFCW-l1O.js.map → orchestrator-CjEau_RG.js.map} +1 -1
  51. package/dist/lib/{orchestrator-BtVj9bBB.js → orchestrator-ClXbJ4Mb.js} +2 -2
  52. package/dist/lib/{orchestrator-BtVj9bBB.js.map → orchestrator-ClXbJ4Mb.js.map} +1 -1
  53. package/dist/lib/{peerjs-BoBkx9DJ.js → peerjs-BdfpltIe.js} +3 -3
  54. package/dist/lib/{peerjs-BoBkx9DJ.js.map → peerjs-BdfpltIe.js.map} +1 -1
  55. package/dist/lib/{push-client-D6hbzDNV.js → push-client-DjwKknNg.js} +9 -3
  56. package/dist/lib/push-client-DjwKknNg.js.map +1 -0
  57. package/dist/lib/{shadow-claw-Cuf2O5bZ.js → shadow-claw-DcPGKenn.js} +4 -4
  58. package/dist/lib/{shadow-claw-Cuf2O5bZ.js.map → shadow-claw-DcPGKenn.js.map} +1 -1
  59. package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js → shadow-claw-channel-config-DEL-aZ7N.js} +3 -3
  60. package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js.map → shadow-claw-channel-config-DEL-aZ7N.js.map} +1 -1
  61. package/dist/lib/{shadow-claw-channels-BsP5JABM.js → shadow-claw-channels-BgaJiPdE.js} +2 -2
  62. package/dist/lib/{shadow-claw-channels-BsP5JABM.js.map → shadow-claw-channels-BgaJiPdE.js.map} +1 -1
  63. package/dist/lib/{shadow-claw-chat-Cam2lX4k.js → shadow-claw-chat-mXPQZbwT.js} +4 -4
  64. package/dist/lib/{shadow-claw-chat-Cam2lX4k.js.map → shadow-claw-chat-mXPQZbwT.js.map} +1 -1
  65. package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js → shadow-claw-control-plane-DO6foOLV.js} +6 -6
  66. package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js.map → shadow-claw-control-plane-DO6foOLV.js.map} +1 -1
  67. package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js → shadow-claw-conversations-BLwHTwdW.js} +2 -2
  68. package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js.map → shadow-claw-conversations-BLwHTwdW.js.map} +1 -1
  69. package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js → shadow-claw-file-viewer-BSbZvSl-.js} +3 -3
  70. package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js.map → shadow-claw-file-viewer-BSbZvSl-.js.map} +1 -1
  71. package/dist/lib/{shadow-claw-files-DVROUESO.js → shadow-claw-files-BLVr9Pua.js} +2 -2
  72. package/dist/lib/{shadow-claw-files-DVROUESO.js.map → shadow-claw-files-BLVr9Pua.js.map} +1 -1
  73. package/dist/lib/{shadow-claw-git-BqrO7cHJ.js → shadow-claw-git-CHC1H86J.js} +3 -3
  74. package/dist/lib/{shadow-claw-git-BqrO7cHJ.js.map → shadow-claw-git-CHC1H86J.js.map} +1 -1
  75. package/dist/lib/{shadow-claw-llm-BexUQdYo.js → shadow-claw-llm-CKXfxd3D.js} +2 -2
  76. package/dist/lib/{shadow-claw-llm-BexUQdYo.js.map → shadow-claw-llm-CKXfxd3D.js.map} +1 -1
  77. package/dist/lib/{shadow-claw-networking-CfXRGpUD.js → shadow-claw-networking-dpYvfchh.js} +2 -2
  78. package/dist/lib/{shadow-claw-networking-CfXRGpUD.js.map → shadow-claw-networking-dpYvfchh.js.map} +1 -1
  79. package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js → shadow-claw-notifications-_YeAVPga.js} +2 -2
  80. package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js.map → shadow-claw-notifications-_YeAVPga.js.map} +1 -1
  81. package/dist/lib/{shadow-claw-pages-CGzGXmrI.js → shadow-claw-pages-DVzZriZ7.js} +3 -3
  82. package/dist/lib/{shadow-claw-pages-CGzGXmrI.js.map → shadow-claw-pages-DVzZriZ7.js.map} +1 -1
  83. package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js → shadow-claw-peerjs-BvixjeBz.js} +3 -3
  84. package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js.map → shadow-claw-peerjs-BvixjeBz.js.map} +1 -1
  85. package/dist/lib/{shadow-claw-settings-DBfMZdDS.js → shadow-claw-settings-DV9-gD4Q.js} +2 -2
  86. package/dist/lib/{shadow-claw-settings-DBfMZdDS.js.map → shadow-claw-settings-DV9-gD4Q.js.map} +1 -1
  87. package/dist/lib/{shadow-claw-storage-CM-w6mgG.js → shadow-claw-storage-Bv__kXG6.js} +2 -2
  88. package/dist/lib/{shadow-claw-storage-CM-w6mgG.js.map → shadow-claw-storage-Bv__kXG6.js.map} +1 -1
  89. package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js → shadow-claw-task-server-DNOe8PWK.js} +2 -2
  90. package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js.map → shadow-claw-task-server-DNOe8PWK.js.map} +1 -1
  91. package/dist/lib/{shadow-claw-tasks-K-foOHSE.js → shadow-claw-tasks-qmcDT4PU.js} +3 -3
  92. package/dist/lib/{shadow-claw-tasks-K-foOHSE.js.map → shadow-claw-tasks-qmcDT4PU.js.map} +1 -1
  93. package/dist/lib/{shadow-claw-terminal-CpAknKyA.js → shadow-claw-terminal-0QUWP8of.js} +2 -2
  94. package/dist/lib/{shadow-claw-terminal-CpAknKyA.js.map → shadow-claw-terminal-0QUWP8of.js.map} +1 -1
  95. package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js → shadow-claw-tools-Bs_bmI6-.js} +3 -3
  96. package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js.map → shadow-claw-tools-Bs_bmI6-.js.map} +1 -1
  97. package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js → shadow-claw-webvm-DYaoL7QY.js} +2 -2
  98. package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js.map → shadow-claw-webvm-DYaoL7QY.js.map} +1 -1
  99. package/dist/lib/{syncWebMcpRegistration-NspKefOI.js → syncWebMcpRegistration-BhbqI6R2.js} +2 -2
  100. package/dist/lib/{syncWebMcpRegistration-NspKefOI.js.map → syncWebMcpRegistration-BhbqI6R2.js.map} +1 -1
  101. package/dist/public/.well-known/mcp/server-card.json +1 -1
  102. package/dist/public/.well-known/mcp.json +1 -1
  103. package/dist/public/AGENTS.md +22 -3
  104. package/dist/public/README.md +96 -278
  105. package/dist/public/agent.worker.js +50 -50
  106. package/dist/public/{base-task-model-CYkpwnvU-DDfHUUIS.js → base-task-model-CYkpwnvU-DrXr4G1R.js} +1 -1
  107. package/dist/public/{configurePeerJs-BhBokQlC.js → configurePeerJs-B470pdeU.js} +1 -1
  108. package/dist/public/docs/README.md +31 -31
  109. package/dist/public/docs/architecture/context-management.md +7 -0
  110. package/dist/public/docs/architecture/storage.md +10 -1
  111. package/dist/public/docs/decisions/bundled-typescript-architecture.md +0 -1
  112. package/dist/public/docs/decisions/native-web-components-and-signals.md +0 -1
  113. package/dist/public/docs/example/article/index.html +1 -1
  114. package/dist/public/docs/guides/publishing-to-github-pages.md +334 -0
  115. package/dist/public/docs/guides/server-development-configuration.md +15 -0
  116. package/dist/public/docs/publishing/index.html +1 -1
  117. package/dist/public/docs/skill-creator/index.html +1 -1
  118. package/dist/public/docs/subsystems/cli.md +10 -2
  119. package/dist/public/docs/subsystems/control-plane.md +9 -8
  120. package/dist/public/docs/subsystems/crypto.md +9 -0
  121. package/dist/public/docs/subsystems/custom-element-security.md +2 -0
  122. package/dist/public/docs/subsystems/mcp-server.md +31 -16
  123. package/dist/public/docs/subsystems/notifications.md +8 -5
  124. package/dist/public/docs/subsystems/providers.md +9 -0
  125. package/dist/public/docs/subsystems/share-target.md +107 -0
  126. package/dist/public/docs/subsystems/webmcp.md +43 -0
  127. package/dist/public/e2e/README.md +4 -2
  128. package/dist/public/{e2e-bridge-DcBoeFvd.js → e2e-bridge-CSW0S5Yz.js} +1 -1
  129. package/dist/public/{file-viewer-CKtVC9Ix.js → file-viewer-Cs-riNvy.js} +1 -1
  130. package/dist/public/{firebase-OSDX3viP.js → firebase-cwAl_ABm.js} +1 -1
  131. package/dist/public/{git-CkhCJonj.js → git-GD09WATp.js} +25 -25
  132. package/dist/public/index.html +1 -1
  133. package/dist/public/index.js +1 -1
  134. package/dist/public/{initControlPlane-Bi8ouFEb.js → initControlPlane-BqCY0NQy.js} +1 -1
  135. package/dist/public/initControlPlane-C-IXaKYX.js +1 -0
  136. package/dist/public/{language-detector-BRKrlra0.js → language-detector-CIeEuQFk.js} +1 -1
  137. package/dist/public/llms.txt +4 -2
  138. package/dist/public/main/index.html +1 -1
  139. package/dist/public/main/memory/index.html +1 -1
  140. package/dist/public/{markdown-C-hCVJtc.js → markdown-Cw7vCtTg.js} +1 -1
  141. package/dist/public/{openai-Bpd71Oja.js → openai-BJIb2c5j.js} +3 -3
  142. package/dist/public/{orchestrator-8qNGfw8r.js → orchestrator-BWkHHO9Y.js} +1 -1
  143. package/dist/public/{orchestrator-BT3Ks5ip.js → orchestrator-DpkkeD6T.js} +1 -1
  144. package/dist/public/{peerjs-DfhwEiiq.js → peerjs-tf2EmWjB.js} +1 -1
  145. package/dist/public/{prompt-api-polyfill-Dl1kbJhR.js → prompt-api-polyfill-CXd75leW.js} +1 -1
  146. package/dist/public/{push-client-BVLrE9pK.js → push-client-CHjSbrWs.js} +1 -1
  147. package/dist/public/{rewriter-j6D9LDER.js → rewriter-BEHn9gll.js} +1 -1
  148. package/dist/public/service-worker.js +1 -1
  149. package/dist/public/{shadow-claw-DCjxNDCX.js → shadow-claw-BBPV_9X8.js} +2 -2
  150. package/dist/public/{shadow-claw-channels-CITDrqMl.js → shadow-claw-channels-BJvrKQjg.js} +1 -1
  151. package/dist/public/{shadow-claw-chat-BQrSgSKF.js → shadow-claw-chat-BG2EaRaw.js} +1 -1
  152. package/dist/public/{shadow-claw-control-plane-Cp001OY0.js → shadow-claw-control-plane-BBa7WbTg.js} +2 -2
  153. package/dist/public/{shadow-claw-conversations-EhX4RvIj.js → shadow-claw-conversations-CFaWFmkm.js} +1 -1
  154. package/dist/public/{shadow-claw-file-viewer-DiQXIjZD.js → shadow-claw-file-viewer-Z-GIKaE9.js} +1 -1
  155. package/dist/public/{shadow-claw-files-lkTnSJsn.js → shadow-claw-files-BBx4iMsk.js} +1 -1
  156. package/dist/public/{shadow-claw-git-BlcA_z8x.js → shadow-claw-git-vTOKatwd.js} +2 -2
  157. package/dist/public/{shadow-claw-llm-CW16X9Rg.js → shadow-claw-llm-Ce1Ax6-x.js} +1 -1
  158. package/dist/public/{shadow-claw-networking-ySsMBY_i.js → shadow-claw-networking-q4XIzdB5.js} +1 -1
  159. package/dist/public/{shadow-claw-notifications-CyV2zCQ8.js → shadow-claw-notifications-D2cz0gmm.js} +1 -1
  160. package/dist/public/{shadow-claw-pages-Cx7cF9ah.js → shadow-claw-pages-3335lmSC.js} +1 -1
  161. package/dist/public/{shadow-claw-settings-zm4PHHGi.js → shadow-claw-settings-4ptYuJwO.js} +2 -2
  162. package/dist/public/{shadow-claw-storage-dy9JAEGd.js → shadow-claw-storage-bIrPKFhF.js} +1 -1
  163. package/dist/public/{shadow-claw-task-server-Bleww48s.js → shadow-claw-task-server-5yqVBxy1.js} +1 -1
  164. package/dist/public/{shadow-claw-tasks-CTI5oaLN.js → shadow-claw-tasks-DcqYHPLZ.js} +1 -1
  165. package/dist/public/{shadow-claw-terminal-mZ3i4HaQ.js → shadow-claw-terminal-fARaDONr.js} +1 -1
  166. package/dist/public/{shadow-claw-tools-B_nuk8Zl.js → shadow-claw-tools-paEAhcJr.js} +1 -1
  167. package/dist/public/{shadow-claw-webvm-Vl_jH5YL.js → shadow-claw-webvm-CbzH_vyX.js} +1 -1
  168. package/dist/public/{summarizer-VJNr-7xG.js → summarizer-BT3AuFed.js} +1 -1
  169. package/dist/public/{syncWebMcpRegistration-n2vAMJ4w.js → syncWebMcpRegistration-CWSYaHex.js} +1 -1
  170. package/dist/public/{transformers-BhQMWQG5.js → transformers-1VTxqKjT.js} +1 -1
  171. package/dist/public/{translator-DEQXb7ZL.js → translator-B50RYlFt.js} +1 -1
  172. package/dist/public/{webllm-CHN5H-Sy.js → webllm-FmyJWWpt.js} +1 -1
  173. package/dist/public/{writer-CD9oN2pq.js → writer-Bhb4YYRE.js} +1 -1
  174. package/dist/server.js +205 -203
  175. package/package.json +1 -1
  176. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +2 -0
  177. package/src/core/orchestrator/utils/configurePeerJs.test.ts +2 -0
  178. package/src/server/client-registry.test.ts +9 -0
  179. package/src/server/client-registry.ts +14 -0
  180. package/src/server/mcp/mcp-integration.test.ts +68 -16
  181. package/src/server/mcp/tools/built-in-tools.ts +125 -34
  182. package/src/server/mcp/tools/client-tool-relay.ts +81 -44
  183. package/src/server/mcp/tools/mcp-tools.test.ts +103 -13
  184. package/src/server/mcp/types.ts +3 -0
  185. package/src/server/openapi/paths/push.ts +48 -0
  186. package/src/server/server.ts +30 -0
  187. package/src/subsystems/notifications/push-client.test.ts +17 -0
  188. package/src/subsystems/notifications/push-client.ts +22 -2
  189. package/src/subsystems/notifications/push-routes.test.ts +31 -0
  190. package/src/subsystems/notifications/push-routes.ts +10 -0
  191. package/src/subsystems/notifications/push-store.test.ts +49 -0
  192. package/src/subsystems/notifications/push-store.ts +45 -0
  193. package/src/worker/worker.test.ts +2 -0
  194. package/dist/lib/push-client-D6hbzDNV.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadow-claw",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0",
6
6
  "description": "Browser-native personal AI assistant.",
@@ -58,6 +58,8 @@ jest.unstable_mockModule("../../../config/config.js", () => ({
58
58
  TRANSFORMERS_JS_DTYPE_STRATEGY: "transformers_js_dtype_strategy",
59
59
  },
60
60
  DEFAULT_MAX_ITERATIONS: 50,
61
+ DEFAULT_CONTROL_PLANE_ENABLED: false,
62
+ DEFAULT_CONTROL_PLANE_TRANSPORT: "http",
61
63
  DEFAULT_SUBAGENT_MAX_PARALLEL: 5,
62
64
  DEFAULT_SUBAGENT_WORKSPACE_MODE: "automatic",
63
65
  DEFAULT_PROMPT_API_FALLBACK_MODEL: "onnx-community/Qwen3-0.6B-ONNX",
@@ -60,6 +60,8 @@ const mockConfig = {
60
60
  getModelMaxTokens: jest.fn().mockReturnValue(128000),
61
61
  buildTriggerPattern: jest.fn().mockReturnValue(new RegExp("")),
62
62
  DEFAULT_GROUP_ID: "br:main",
63
+ DEFAULT_CONTROL_PLANE_ENABLED: false,
64
+ DEFAULT_CONTROL_PLANE_TRANSPORT: "http",
63
65
  OPFS_ROOT: "shadowclaw",
64
66
  LLAMAFILE_PROXY_URL: "/proxy/llamafile",
65
67
  };
@@ -14,6 +14,7 @@ import {
14
14
  getControlTokenFilePath,
15
15
  } from "./client-registry.js";
16
16
  import fs from "node:fs";
17
+ import { tmpdir } from "node:os";
17
18
  import path from "node:path";
18
19
 
19
20
  beforeEach(() => {
@@ -246,5 +247,13 @@ describe("client-registry", () => {
246
247
  expect(rawJson.token).toBe("my-secret-test-token");
247
248
  expect(rawJson.createdAt).toBe(1700000000000);
248
249
  });
250
+
251
+ it("mirrors saved token to system tmp directory", () => {
252
+ saveControlTokenFile("tmp-mirrored-token", tempCacheDir, 1700000000000);
253
+ const tmpFile = path.join(tmpdir(), "shadow-claw", "control-token.json");
254
+ expect(fs.existsSync(tmpFile)).toBe(true);
255
+ const content = JSON.parse(fs.readFileSync(tmpFile, "utf8"));
256
+ expect(content.token).toBe("tmp-mirrored-token");
257
+ });
249
258
  });
250
259
  });
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { randomBytes } from "node:crypto";
9
9
  import fs from "node:fs";
10
+ import { tmpdir } from "node:os";
10
11
  import path from "node:path";
11
12
  import { DatabaseSync } from "node:sqlite";
12
13
  import type { ClientInfo, BackupRecord } from "./control-plane-types.js";
@@ -281,6 +282,19 @@ export function saveControlTokenFile(
281
282
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
282
283
  } catch (_) {}
283
284
 
285
+ // Mirror to system tmp directory so CLI commands run from any working directory can locate it
286
+ try {
287
+ const tmpCacheDir = path.join(tmpdir(), "shadow-claw");
288
+ if (!fs.existsSync(tmpCacheDir)) {
289
+ fs.mkdirSync(tmpCacheDir, { recursive: true });
290
+ }
291
+ fs.writeFileSync(
292
+ path.join(tmpCacheDir, "control-token.json"),
293
+ JSON.stringify(payload, null, 2) + "\n",
294
+ "utf8",
295
+ );
296
+ } catch (_) {}
297
+
284
298
  return payload;
285
299
  }
286
300
 
@@ -239,17 +239,18 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
239
239
  const toolNames = res.body.result.tools.map((t: any) => t.name);
240
240
 
241
241
  // Built-in tools
242
- expect(toolNames).toContain("shadowclaw_list_clients");
243
- expect(toolNames).toContain("shadowclaw_send_message");
244
- expect(toolNames).toContain("shadowclaw_read_state");
245
- expect(toolNames).toContain("shadowclaw_list_tasks");
242
+ expect(toolNames).toContain("shadowclaw_server_list_clients");
243
+ expect(toolNames).toContain("shadowclaw_server_send_message");
244
+ expect(toolNames).toContain("shadowclaw_server_read_state");
245
+ expect(toolNames).toContain("shadowclaw_server_list_tasks");
246
246
 
247
247
  // Relayed browser tools
248
- expect(toolNames).toContain("read_file");
249
- expect(toolNames).toContain("workspace_status");
248
+ expect(toolNames).toContain("shadowclaw_client_read_file");
249
+ expect(toolNames).toContain("shadowclaw_client_workspace_status");
250
+ expect(toolNames).toContain("shadowclaw_client_ask_user");
250
251
  });
251
252
 
252
- it("calls built-in tool shadowclaw_read_state and returns browser state", async () => {
253
+ it("calls built-in tool shadowclaw_server_read_state and returns browser state", async () => {
253
254
  const res = await postMcp(
254
255
  port,
255
256
  token,
@@ -258,11 +259,14 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
258
259
  id: 3,
259
260
  method: "tools/call",
260
261
  params: {
261
- name: "shadowclaw_read_state",
262
+ name: "shadowclaw_server_read_state",
262
263
  arguments: { clientId: "test-browser-client-e2e" },
263
264
  },
264
265
  },
265
- { "mcp-method": "tools/call", "mcp-name": "shadowclaw_read_state" },
266
+ {
267
+ "mcp-method": "tools/call",
268
+ "mcp-name": "shadowclaw_server_read_state",
269
+ },
266
270
  );
267
271
 
268
272
  expect(res.status).toBe(200);
@@ -270,9 +274,28 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
270
274
  const content = res.body.result.content[0].text;
271
275
  expect(content).toContain("br:main");
272
276
  expect(content).toContain("claude-3-5-sonnet");
277
+
278
+ // Calling via legacy alias shadowclaw_read_state also succeeds
279
+ const resAlias = await postMcp(
280
+ port,
281
+ token,
282
+ {
283
+ jsonrpc: "2.0",
284
+ id: 31,
285
+ method: "tools/call",
286
+ params: {
287
+ name: "shadowclaw_read_state",
288
+ arguments: { clientId: "test-browser-client-e2e" },
289
+ },
290
+ },
291
+ { "mcp-method": "tools/call", "mcp-name": "shadowclaw_read_state" },
292
+ );
293
+ expect(resAlias.status).toBe(200);
294
+ expect(resAlias.body.result.resultType).toBe("complete");
295
+ expect(resAlias.body.result.content[0].text).toContain("br:main");
273
296
  });
274
297
 
275
- it("calls relayed browser tool read_file across Control Plane and receives response", async () => {
298
+ it("calls relayed browser tool shadowclaw_client_read_file across Control Plane and receives response", async () => {
276
299
  const res = await postMcp(
277
300
  port,
278
301
  token,
@@ -281,11 +304,14 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
281
304
  id: 4,
282
305
  method: "tools/call",
283
306
  params: {
284
- name: "read_file",
307
+ name: "shadowclaw_client_read_file",
285
308
  arguments: { path: "src/main.ts" },
286
309
  },
287
310
  },
288
- { "mcp-method": "tools/call", "mcp-name": "read_file" },
311
+ {
312
+ "mcp-method": "tools/call",
313
+ "mcp-name": "shadowclaw_client_read_file",
314
+ },
289
315
  );
290
316
 
291
317
  expect(res.status).toBe(200);
@@ -295,6 +321,26 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
295
321
  expect(text).toContain(
296
322
  "Content of file src/main.ts: Hello from Browser OPFS!",
297
323
  );
324
+
325
+ // Also verify backward compatibility: calling with unprefixed name also proxies
326
+ const resUnprefixed = await postMcp(
327
+ port,
328
+ token,
329
+ {
330
+ jsonrpc: "2.0",
331
+ id: 41,
332
+ method: "tools/call",
333
+ params: {
334
+ name: "read_file",
335
+ arguments: { path: "src/main.ts" },
336
+ },
337
+ },
338
+ { "mcp-method": "tools/call", "mcp-name": "read_file" },
339
+ );
340
+ expect(resUnprefixed.status).toBe(200);
341
+ expect(resUnprefixed.body.result.content[0].text).toContain(
342
+ "Content of file src/main.ts: Hello from Browser OPFS!",
343
+ );
298
344
  });
299
345
 
300
346
  it("handles MRTR interactive flow for ask_user tool", async () => {
@@ -307,11 +353,14 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
307
353
  id: 5,
308
354
  method: "tools/call",
309
355
  params: {
310
- name: "ask_user",
356
+ name: "shadowclaw_client_ask_user",
311
357
  arguments: { question: "Are you ready?" },
312
358
  },
313
359
  },
314
- { "mcp-method": "tools/call", "mcp-name": "ask_user" },
360
+ {
361
+ "mcp-method": "tools/call",
362
+ "mcp-name": "shadowclaw_client_ask_user",
363
+ },
315
364
  );
316
365
 
317
366
  expect(res1.status).toBe(200);
@@ -328,12 +377,15 @@ describe("End-to-End MCP Server & Control Plane Relay", () => {
328
377
  id: 6,
329
378
  method: "tools/call",
330
379
  params: {
331
- name: "ask_user",
380
+ name: "shadowclaw_client_ask_user",
332
381
  arguments: { question: "Are you ready?" },
333
382
  inputResponses: { response: "Yes, ready to proceed!" },
334
383
  },
335
384
  },
336
- { "mcp-method": "tools/call", "mcp-name": "ask_user" },
385
+ {
386
+ "mcp-method": "tools/call",
387
+ "mcp-name": "shadowclaw_client_ask_user",
388
+ },
337
389
  );
338
390
 
339
391
  expect(res2.status).toBe(200);
@@ -7,12 +7,100 @@
7
7
 
8
8
  import type { McpServer } from "../mcp-server.js";
9
9
  import type { McpTool } from "../types.js";
10
- import { broadcastPush } from "../../../subsystems/notifications/push-routes.js";
10
+ import {
11
+ broadcastPush,
12
+ getRegisteredPushClients,
13
+ type PushClientRecord,
14
+ } from "../../../subsystems/notifications/push-routes.js";
11
15
  import { getPackageVersion } from "../../utils/packageVersion.js";
12
16
 
17
+ export function buildSendNotificationTool(
18
+ pushClients: Array<
19
+ PushClientRecord | { clientId: string; deviceLabel?: string }
20
+ > = [],
21
+ name: string = "shadowclaw_server_send_notification",
22
+ ): McpTool {
23
+ const sortedPushClients = [...pushClients].sort((a, b) =>
24
+ a.clientId.localeCompare(b.clientId),
25
+ );
26
+ const pushClientIds = sortedPushClients
27
+ .map((c) => c.clientId)
28
+ .filter(Boolean);
29
+
30
+ const baseDescription =
31
+ "Broadcast an OS-level push notification to subscribed devices via Web Push (VAPID), or send to a specific registered client. Works even when the client browser tab is closed, asleep, or running in the background.";
32
+
33
+ const clientIdProp: any = {
34
+ type: "string",
35
+ description:
36
+ pushClientIds.length > 0
37
+ ? `Target client ID, prefix, or device label of a specific client registered for push notifications (optional; if omitted, broadcasts to all subscribed devices). Available on: ${sortedPushClients.map((c) => `${c.clientId} (${c.deviceLabel || "Client"})`).join(", ")}`
38
+ : "Target client ID, prefix, or device label of a specific client that has registered in the past. If omitted, broadcasts to all subscribed devices.",
39
+ };
40
+
41
+ if (pushClientIds.length > 0) {
42
+ clientIdProp.enum = pushClientIds;
43
+ }
44
+
45
+ return {
46
+ name,
47
+ description: baseDescription,
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ title: {
52
+ type: "string",
53
+ description: "Notification title (default: 'ShadowClaw').",
54
+ },
55
+ body: {
56
+ type: "string",
57
+ description: "Notification body message text.",
58
+ },
59
+ clientId: clientIdProp,
60
+ },
61
+ required: ["body"],
62
+ },
63
+ };
64
+ }
65
+
66
+ export function getDynamicSendNotificationTool(
67
+ controlPlane?: any,
68
+ name: string = "shadowclaw_server_send_notification",
69
+ ): McpTool {
70
+ let pushClients: PushClientRecord[] = [];
71
+ try {
72
+ pushClients = getRegisteredPushClients();
73
+ } catch (_) {}
74
+
75
+ if (controlPlane && Array.isArray(pushClients) && pushClients.length > 0) {
76
+ try {
77
+ const connected =
78
+ typeof controlPlane.getConnectedClients === "function"
79
+ ? controlPlane.getConnectedClients()
80
+ : typeof controlPlane.listClients === "function"
81
+ ? controlPlane.listClients()
82
+ : [];
83
+ if (Array.isArray(connected)) {
84
+ for (const pc of pushClients) {
85
+ if (!pc.deviceLabel) {
86
+ const match = connected.find(
87
+ (c: any) => (c.clientId || c.id) === pc.clientId,
88
+ );
89
+ if (match && match.deviceLabel) {
90
+ pc.deviceLabel = match.deviceLabel;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ } catch (_) {}
96
+ }
97
+
98
+ return buildSendNotificationTool(pushClients, name);
99
+ }
100
+
13
101
  export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
14
102
  {
15
- name: "shadowclaw_list_clients",
103
+ name: "shadowclaw_server_list_clients",
16
104
  description:
17
105
  "List connected browser and Electron clients, including device type, ID, and active capabilities.",
18
106
  inputSchema: {
@@ -21,7 +109,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
21
109
  },
22
110
  },
23
111
  {
24
- name: "shadowclaw_send_message",
112
+ name: "shadowclaw_server_send_message",
25
113
  description:
26
114
  "Send a message or prompt to a connected ShadowClaw client's active AI orchestrator queue.",
27
115
  inputSchema: {
@@ -46,7 +134,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
46
134
  },
47
135
  },
48
136
  {
49
- name: "shadowclaw_read_state",
137
+ name: "shadowclaw_server_read_state",
50
138
  description:
51
139
  "Read the current orchestrator state (idle, responding, etc.), active group ID, and capabilities from a connected client.",
52
140
  inputSchema: {
@@ -61,7 +149,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
61
149
  },
62
150
  },
63
151
  {
64
- name: "shadowclaw_list_tasks",
152
+ name: "shadowclaw_server_list_tasks",
65
153
  description:
66
154
  "List scheduled background tasks configured on a connected client (optionally filtered by conversation group).",
67
155
  inputSchema: {
@@ -80,7 +168,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
80
168
  },
81
169
  },
82
170
  {
83
- name: "shadowclaw_manage_backup",
171
+ name: "shadowclaw_server_manage_backup",
84
172
  description:
85
173
  "Trigger or manage OPFS workspace file backups for a connected client (trigger, list, or delete).",
86
174
  inputSchema: {
@@ -108,7 +196,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
108
196
  },
109
197
  },
110
198
  {
111
- name: "shadowclaw_set_active_client",
199
+ name: "shadowclaw_server_set_active_client",
112
200
  description:
113
201
  "Set the active default connected client used when no clientId is explicitly provided in tool calls.",
114
202
  inputSchema: {
@@ -132,30 +220,7 @@ export const SHADOWCLAW_BUILTIN_TOOLS: McpTool[] = [
132
220
  properties: {},
133
221
  },
134
222
  },
135
- {
136
- name: "shadowclaw_send_notification",
137
- description:
138
- "Broadcast an OS-level push notification to subscribed devices via Web Push (VAPID), or send to a specific registered client. Works even when the client browser tab is closed, asleep, or running in the background.",
139
- inputSchema: {
140
- type: "object",
141
- properties: {
142
- title: {
143
- type: "string",
144
- description: "Notification title (default: 'ShadowClaw').",
145
- },
146
- body: {
147
- type: "string",
148
- description: "Notification body message text.",
149
- },
150
- clientId: {
151
- type: "string",
152
- description:
153
- "Target client ID, prefix, or device label of a specific client that has registered in the past. If omitted, broadcasts to all subscribed devices.",
154
- },
155
- },
156
- required: ["body"],
157
- },
158
- },
223
+ buildSendNotificationTool(),
159
224
  ];
160
225
 
161
226
  export function resolveTargetClientId(
@@ -214,9 +279,14 @@ export function registerBuiltInTools(
214
279
  server: McpServer,
215
280
  controlPlane: any,
216
281
  ): void {
282
+ server.setToolProvider(async () => {
283
+ return [getDynamicSendNotificationTool(controlPlane)];
284
+ });
285
+
217
286
  for (const tool of SHADOWCLAW_BUILTIN_TOOLS) {
218
- server.registerTool(tool, async (args) => {
287
+ const handler = async (args: Record<string, any>) => {
219
288
  switch (tool.name) {
289
+ case "shadowclaw_server_list_clients":
220
290
  case "shadowclaw_list_clients": {
221
291
  const clients =
222
292
  typeof controlPlane.getConnectedClients === "function"
@@ -235,6 +305,7 @@ export function registerBuiltInTools(
235
305
  };
236
306
  }
237
307
 
308
+ case "shadowclaw_server_send_message":
238
309
  case "shadowclaw_send_message": {
239
310
  const text = String(args.text || "").trim();
240
311
  if (!text) {
@@ -278,6 +349,7 @@ export function registerBuiltInTools(
278
349
  };
279
350
  }
280
351
 
352
+ case "shadowclaw_server_read_state":
281
353
  case "shadowclaw_read_state": {
282
354
  const targetId = resolveTargetClientId(controlPlane, args.clientId);
283
355
  if (!targetId) {
@@ -308,6 +380,7 @@ export function registerBuiltInTools(
308
380
  };
309
381
  }
310
382
 
383
+ case "shadowclaw_server_list_tasks":
311
384
  case "shadowclaw_list_tasks": {
312
385
  const targetId = resolveTargetClientId(controlPlane, args.clientId);
313
386
  if (!targetId) {
@@ -337,6 +410,7 @@ export function registerBuiltInTools(
337
410
  };
338
411
  }
339
412
 
413
+ case "shadowclaw_server_manage_backup":
340
414
  case "shadowclaw_manage_backup": {
341
415
  const action = String(args.action || "trigger").toLowerCase();
342
416
  const targetId = resolveTargetClientId(controlPlane, args.clientId);
@@ -426,6 +500,7 @@ export function registerBuiltInTools(
426
500
  };
427
501
  }
428
502
 
503
+ case "shadowclaw_server_set_active_client":
429
504
  case "shadowclaw_set_active_client": {
430
505
  const targetId = resolveTargetClientId(controlPlane, args.clientId);
431
506
  if (!targetId) {
@@ -457,7 +532,8 @@ export function registerBuiltInTools(
457
532
  };
458
533
  }
459
534
 
460
- case "shadowclaw_server_status": {
535
+ case "shadowclaw_server_status":
536
+ case "shadowclaw_status": {
461
537
  const clients =
462
538
  typeof controlPlane.getConnectedClients === "function"
463
539
  ? controlPlane.getConnectedClients()
@@ -482,6 +558,7 @@ export function registerBuiltInTools(
482
558
  };
483
559
  }
484
560
 
561
+ case "shadowclaw_server_send_notification":
485
562
  case "shadowclaw_send_notification":
486
563
  case "send_notification": {
487
564
  const body = String(args.body || "").trim();
@@ -549,6 +626,20 @@ export function registerBuiltInTools(
549
626
  content: [{ type: "text", text: `Unknown tool: '${tool.name}'` }],
550
627
  };
551
628
  }
552
- });
629
+ };
630
+
631
+ server.registerTool(tool, handler);
632
+
633
+ if (tool.name.startsWith("shadowclaw_server_")) {
634
+ const legacyAlias = `shadowclaw_${tool.name.slice("shadowclaw_server_".length)}`;
635
+ server.registerToolHandler(legacyAlias, handler);
636
+ }
637
+ if (
638
+ tool.name === "shadowclaw_server_send_notification" ||
639
+ tool.name === "shadowclaw_send_notification"
640
+ ) {
641
+ server.registerToolHandler("shadowclaw_send_notification", handler);
642
+ server.registerToolHandler("send_notification", handler);
643
+ }
553
644
  }
554
645
  }