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
@@ -7,8 +7,27 @@
7
7
  */
8
8
 
9
9
  import type { McpServer } from "../mcp-server.js";
10
- import type { McpTool, McpToolCallResult } from "../types.js";
11
- import { resolveTargetClientId } from "./built-in-tools.js";
10
+ import {
11
+ MCP_CLIENT_TOOL_PREFIX,
12
+ type McpTool,
13
+ type McpToolCallResult,
14
+ } from "../types.js";
15
+ import {
16
+ resolveTargetClientId,
17
+ getDynamicSendNotificationTool,
18
+ } from "./built-in-tools.js";
19
+
20
+ function getClientRawToolName(name: string): string {
21
+ return name.startsWith(MCP_CLIENT_TOOL_PREFIX)
22
+ ? name.slice(MCP_CLIENT_TOOL_PREFIX.length)
23
+ : name;
24
+ }
25
+
26
+ function toClientExposedToolName(name: string): string {
27
+ return !name.startsWith(MCP_CLIENT_TOOL_PREFIX)
28
+ ? `${MCP_CLIENT_TOOL_PREFIX}${name}`
29
+ : name;
30
+ }
12
31
 
13
32
  export interface ClientToolRelayOptions {
14
33
  targetClientId?: string;
@@ -51,46 +70,44 @@ export function createClientToolRelay(
51
70
  : [];
52
71
  } catch (_) {}
53
72
 
54
- if (!Array.isArray(clients) || clients.length === 0) {
55
- toolSupportingClientsMap.clear();
56
- toolDefMap.clear();
57
- return [];
58
- }
59
-
60
73
  toolSupportingClientsMap.clear();
61
74
  toolDefMap.clear();
62
75
 
63
- for (const c of clients) {
64
- const cid = (c as any).clientId || (c as any).id;
65
- if (!cid) continue;
66
-
67
- try {
68
- const res = await controlPlane.sendCommand(cid, "list-tools", {});
69
- if (
70
- res &&
71
- res.success &&
72
- res.data?.tools &&
73
- Array.isArray(res.data.tools)
74
- ) {
75
- for (const t of res.data.tools) {
76
- if (
77
- t.name === "send_notification" ||
78
- t.name.startsWith("shadowclaw_")
79
- ) {
80
- continue;
81
- }
82
- if (!toolDefMap.has(t.name)) {
83
- toolDefMap.set(t.name, t);
84
- toolSupportingClientsMap.set(t.name, []);
76
+ if (Array.isArray(clients) && clients.length > 0) {
77
+ for (const c of clients) {
78
+ const cid = (c as any).clientId || (c as any).id;
79
+ if (!cid) continue;
80
+
81
+ try {
82
+ const res = await controlPlane.sendCommand(cid, "list-tools", {});
83
+ if (
84
+ res &&
85
+ res.success &&
86
+ res.data?.tools &&
87
+ Array.isArray(res.data.tools)
88
+ ) {
89
+ for (const t of res.data.tools) {
90
+ const rawName = getClientRawToolName(t.name);
91
+ if (
92
+ rawName === "send_notification" ||
93
+ (rawName.startsWith("shadowclaw_") &&
94
+ !rawName.startsWith("shadowclaw_client_"))
95
+ ) {
96
+ continue;
97
+ }
98
+ if (!toolDefMap.has(rawName)) {
99
+ toolDefMap.set(rawName, t);
100
+ toolSupportingClientsMap.set(rawName, []);
101
+ }
102
+ toolSupportingClientsMap.get(rawName)!.push(c);
85
103
  }
86
- toolSupportingClientsMap.get(t.name)!.push(c);
87
104
  }
105
+ } catch (err) {
106
+ console.warn(
107
+ `[ClientToolRelay] Failed to query tools from client ${cid}:`,
108
+ err,
109
+ );
88
110
  }
89
- } catch (err) {
90
- console.warn(
91
- `[ClientToolRelay] Failed to query tools from client ${cid}:`,
92
- err,
93
- );
94
111
  }
95
112
  }
96
113
 
@@ -100,8 +117,8 @@ export function createClientToolRelay(
100
117
  );
101
118
  const toolMap = new Map<string, McpTool>();
102
119
 
103
- for (const [toolName, t] of toolDefMap.entries()) {
104
- const supportingClients = toolSupportingClientsMap.get(toolName) || [];
120
+ for (const [rawToolName, t] of toolDefMap.entries()) {
121
+ const supportingClients = toolSupportingClientsMap.get(rawToolName) || [];
105
122
  const supportingClientIds = supportingClients
106
123
  .map((cl: any) => cl.clientId || cl.id)
107
124
  .filter(Boolean);
@@ -135,10 +152,13 @@ export function createClientToolRelay(
135
152
  : ` [Client: ${toolDefaultLabel} (${toolDefaultId.slice(0, 14)}...)]`
136
153
  : "";
137
154
 
138
- toolMap.set(toolName, {
139
- name: t.name,
155
+ const exposedName = toClientExposedToolName(rawToolName);
156
+
157
+ toolMap.set(exposedName, {
158
+ name: exposedName,
140
159
  description:
141
- (t.description || `Relayed tool '${t.name}' from connected client.`) +
160
+ (t.description ||
161
+ `Relayed tool '${rawToolName}' from connected client.`) +
142
162
  clientNote,
143
163
  inputSchema: {
144
164
  ...existingSchema,
@@ -149,6 +169,9 @@ export function createClientToolRelay(
149
169
  });
150
170
  }
151
171
 
172
+ const dynamicNotifTool = getDynamicSendNotificationTool(controlPlane);
173
+ toolMap.set(dynamicNotifTool.name, dynamicNotifTool);
174
+
152
175
  cachedTools = Array.from(toolMap.values());
153
176
  cacheTime = now;
154
177
  return cachedTools;
@@ -168,6 +191,16 @@ export function createClientToolRelay(
168
191
  },
169
192
  );
170
193
  }
194
+ const rawName = getClientRawToolName(tool.name);
195
+ if (!registeredHandlers.has(rawName)) {
196
+ registeredHandlers.add(rawName);
197
+ server.registerToolHandler(
198
+ rawName,
199
+ async (args, meta, inputResponses) => {
200
+ return executeRelayedTool(rawName, args, meta, inputResponses);
201
+ },
202
+ );
203
+ }
171
204
  }
172
205
  return tools;
173
206
  });
@@ -202,7 +235,11 @@ export function createClientToolRelay(
202
235
  await discoverTools();
203
236
  }
204
237
 
205
- const supportingClients = toolSupportingClientsMap.get(toolName) || [];
238
+ const rawToolName = getClientRawToolName(toolName);
239
+ const supportingClients =
240
+ toolSupportingClientsMap.get(rawToolName) ||
241
+ toolSupportingClientsMap.get(toolName) ||
242
+ [];
206
243
  const supportingClientIds = supportingClients
207
244
  .map((cl: any) => cl.clientId || cl.id)
208
245
  .filter(Boolean);
@@ -253,7 +290,7 @@ export function createClientToolRelay(
253
290
  }
254
291
 
255
292
  // Interactive tool handling: ask_user via MRTR
256
- if (toolName === "ask_user") {
293
+ if (rawToolName === "ask_user" || toolName === "ask_user") {
257
294
  if (!inputResponses || !inputResponses["response"]) {
258
295
  const question =
259
296
  (args.question as string) ||
@@ -302,7 +339,7 @@ export function createClientToolRelay(
302
339
  const toolArgs = { ...args };
303
340
  delete toolArgs.clientId;
304
341
  const res = await controlPlane.sendCommand(clientId, "invoke-tool", {
305
- toolName,
342
+ toolName: rawToolName,
306
343
  input: toolArgs,
307
344
  });
308
345
 
@@ -36,15 +36,17 @@ describe("ShadowClaw Built-in MCP Tools", () => {
36
36
  registerBuiltInTools(server, mockControlPlane);
37
37
 
38
38
  const toolNames = SHADOWCLAW_BUILTIN_TOOLS.map((t) => t.name);
39
- expect(toolNames).toContain("shadowclaw_list_clients");
40
- expect(toolNames).toContain("shadowclaw_send_message");
41
- expect(toolNames).toContain("shadowclaw_read_state");
42
- expect(toolNames).toContain("shadowclaw_list_tasks");
43
- expect(toolNames).toContain("shadowclaw_manage_backup");
44
- expect(toolNames).toContain("shadowclaw_send_notification");
39
+ expect(toolNames).toContain("shadowclaw_server_list_clients");
40
+ expect(toolNames).toContain("shadowclaw_server_send_message");
41
+ expect(toolNames).toContain("shadowclaw_server_read_state");
42
+ expect(toolNames).toContain("shadowclaw_server_list_tasks");
43
+ expect(toolNames).toContain("shadowclaw_server_manage_backup");
44
+ expect(toolNames).toContain("shadowclaw_server_set_active_client");
45
+ expect(toolNames).toContain("shadowclaw_server_status");
46
+ expect(toolNames).toContain("shadowclaw_server_send_notification");
45
47
  });
46
48
 
47
- it("executes shadowclaw_list_clients", async () => {
49
+ it("executes shadowclaw_server_list_clients", async () => {
48
50
  const server = new McpServer();
49
51
  const mockControlPlane: any = {
50
52
  getConnectedClients: jest.fn().mockReturnValue([
@@ -65,7 +67,7 @@ describe("ShadowClaw Built-in MCP Tools", () => {
65
67
  id: 1,
66
68
  method: "tools/call",
67
69
  params: {
68
- name: "shadowclaw_list_clients",
70
+ name: "shadowclaw_server_list_clients",
69
71
  arguments: {},
70
72
  },
71
73
  });
@@ -77,6 +79,21 @@ describe("ShadowClaw Built-in MCP Tools", () => {
77
79
  expect(text).toContain("client-abc");
78
80
  expect(text).toContain("MacBook Chrome");
79
81
  }
82
+
83
+ // Legacy alias also works
84
+ const resAlias = await server.handleRequest({
85
+ jsonrpc: "2.0",
86
+ id: 11,
87
+ method: "tools/call",
88
+ params: {
89
+ name: "shadowclaw_list_clients",
90
+ arguments: {},
91
+ },
92
+ });
93
+ expect(resAlias).not.toBeNull();
94
+ if (resAlias && "result" in resAlias) {
95
+ expect(resAlias.result.content[0].text).toContain("client-abc");
96
+ }
80
97
  });
81
98
 
82
99
  it("executes shadowclaw_send_message to active client", async () => {
@@ -481,6 +498,60 @@ describe("ShadowClaw Built-in MCP Tools", () => {
481
498
  closePushStore();
482
499
  }
483
500
  });
501
+
502
+ it("dynamically populates clientId enum and description for shadowclaw_server_send_notification when push clients are registered", async () => {
503
+ openPushStore(":memory:");
504
+ try {
505
+ saveSubscription({
506
+ endpoint: "https://fcm.googleapis.com/fcm/send/device1",
507
+ keys: { p256dh: "k1", auth: "a1" },
508
+ clientId: "client-push-001",
509
+ deviceLabel: "Pixel 9 Pro",
510
+ });
511
+ saveSubscription({
512
+ endpoint: "https://fcm.googleapis.com/fcm/send/device2",
513
+ keys: { p256dh: "k2", auth: "a2" },
514
+ clientId: "client-push-002",
515
+ deviceLabel: "MacBook Air",
516
+ });
517
+
518
+ const server = new McpServer();
519
+ const mockControlPlane: any = {
520
+ getConnectedClients: jest.fn().mockReturnValue([]),
521
+ };
522
+ registerBuiltInTools(server, mockControlPlane);
523
+ const relay = createClientToolRelay(mockControlPlane);
524
+ relay.attachToServer(server);
525
+
526
+ const res = await server.handleRequest({
527
+ jsonrpc: "2.0",
528
+ id: 99,
529
+ method: "tools/list",
530
+ });
531
+
532
+ expect(res).not.toBeNull();
533
+ if (res && "result" in res) {
534
+ const notifTool = res.result.tools.find(
535
+ (t: any) => t.name === "shadowclaw_server_send_notification",
536
+ );
537
+ expect(notifTool).toBeDefined();
538
+ const clientIdProp = notifTool.inputSchema.properties.clientId;
539
+ expect(clientIdProp).toBeDefined();
540
+ expect(clientIdProp.enum).toEqual([
541
+ "client-push-001",
542
+ "client-push-002",
543
+ ]);
544
+ expect(clientIdProp.description).toContain(
545
+ "client-push-001 (Pixel 9 Pro)",
546
+ );
547
+ expect(clientIdProp.description).toContain(
548
+ "client-push-002 (MacBook Air)",
549
+ );
550
+ }
551
+ } finally {
552
+ closePushStore();
553
+ }
554
+ });
484
555
  });
485
556
 
486
557
  describe("Client Tool Relay", () => {
@@ -528,7 +599,9 @@ describe("Client Tool Relay", () => {
528
599
 
529
600
  // Tools list should discover relayed tool
530
601
  const tools = await server.getTools();
531
- const readFileTool = tools.find((t) => t.name === "read_file");
602
+ const readFileTool = tools.find(
603
+ (t) => t.name === "shadowclaw_client_read_file",
604
+ );
532
605
  expect(readFileTool).toBeDefined();
533
606
  expect(readFileTool?.description).toBe("Read workspace file");
534
607
 
@@ -538,7 +611,7 @@ describe("Client Tool Relay", () => {
538
611
  id: 3,
539
612
  method: "tools/call",
540
613
  params: {
541
- name: "read_file",
614
+ name: "shadowclaw_client_read_file",
542
615
  arguments: { path: "notes.txt" },
543
616
  },
544
617
  });
@@ -548,6 +621,21 @@ describe("Client Tool Relay", () => {
548
621
  expect(res.result.resultType).toBe("complete");
549
622
  expect(res.result.content[0].text).toContain("File content here");
550
623
  }
624
+
625
+ // Calling via unprefixed name also works as backward-compatible alias
626
+ const resAlias = await server.handleRequest({
627
+ jsonrpc: "2.0",
628
+ id: 31,
629
+ method: "tools/call",
630
+ params: {
631
+ name: "read_file",
632
+ arguments: { path: "notes.txt" },
633
+ },
634
+ });
635
+ expect(resAlias).not.toBeNull();
636
+ if (resAlias && "result" in resAlias) {
637
+ expect(resAlias.result.content[0].text).toContain("File content here");
638
+ }
551
639
  });
552
640
 
553
641
  it("handles ask_user interactive MRTR loop via client relay", async () => {
@@ -802,7 +890,9 @@ describe("Client Tool Relay", () => {
802
890
 
803
891
  // 1. Discover tools across multi-client: schema should have clientId enum
804
892
  const tools = await server.getTools();
805
- const listFilesTool = tools.find((t) => t.name === "list_files");
893
+ const listFilesTool = tools.find(
894
+ (t) => t.name === "shadowclaw_client_list_files",
895
+ );
806
896
  expect(listFilesTool).toBeDefined();
807
897
  const props = listFilesTool?.inputSchema.properties as any;
808
898
  expect(props?.clientId).toBeDefined();
@@ -828,7 +918,7 @@ describe("Client Tool Relay", () => {
828
918
  id: 21,
829
919
  method: "tools/call",
830
920
  params: {
831
- name: "list_files",
921
+ name: "shadowclaw_client_list_files",
832
922
  arguments: { clientId: "c1" },
833
923
  },
834
924
  });
@@ -843,7 +933,7 @@ describe("Client Tool Relay", () => {
843
933
  id: 22,
844
934
  method: "tools/call",
845
935
  params: {
846
- name: "list_files",
936
+ name: "shadowclaw_client_list_files",
847
937
  arguments: {},
848
938
  },
849
939
  });
@@ -19,6 +19,9 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [
19
19
  export type SupportedProtocolVersion =
20
20
  (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
21
21
 
22
+ export const MCP_CLIENT_TOOL_PREFIX = "shadowclaw_client_";
23
+ export const MCP_SERVER_TOOL_PREFIX = "shadowclaw_server_";
24
+
22
25
  // Standard JSON-RPC and MCP error codes
23
26
  export const MCP_ERROR_CODES = {
24
27
  ParseError: -32700,
@@ -101,6 +101,54 @@ export const pushPaths: Record<string, OpenApiPathItem> = {
101
101
  },
102
102
  },
103
103
  },
104
+ "/push/clients": {
105
+ get: {
106
+ tags: ["Push Notifications"],
107
+ summary: "List Registered Push Clients",
108
+ description:
109
+ "Retrieves unique registered clients that have active Web Push subscriptions, including device labels and subscription counts.",
110
+ responses: {
111
+ "200": {
112
+ description: "List of registered push clients",
113
+ content: {
114
+ "application/json": {
115
+ schema: {
116
+ type: "object",
117
+ properties: {
118
+ clients: {
119
+ type: "array",
120
+ items: {
121
+ type: "object",
122
+ required: ["clientId"],
123
+ properties: {
124
+ clientId: {
125
+ type: "string",
126
+ description: "Unique client ID",
127
+ },
128
+ deviceLabel: {
129
+ type: "string",
130
+ description: "Client device label",
131
+ },
132
+ subscriptionCount: {
133
+ type: "integer",
134
+ description: "Number of active push subscriptions",
135
+ },
136
+ lastSeen: {
137
+ type: "string",
138
+ description:
139
+ "Timestamp of latest subscription creation",
140
+ },
141
+ },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ },
150
+ },
151
+ },
104
152
  "/push/send": {
105
153
  post: {
106
154
  tags: ["Push Notifications"],
@@ -2,6 +2,8 @@ import { exit } from "node:process";
2
2
  import http from "node:http";
3
3
  import https from "node:https";
4
4
  import path from "node:path";
5
+ import fs from "node:fs";
6
+ import { tmpdir } from "node:os";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import tcpPortUsed from "tcp-port-used";
7
9
 
@@ -116,6 +118,34 @@ export async function startServer(
116
118
  `Control plane active at ${protocol}://${config.bindHost}:${config.port}/api/control/events (SSE) and ${wsProtocol}://${config.bindHost}:${config.port}/ws/control (WebSocket)`,
117
119
  );
118
120
  console.log(`Control token: ${controlPlane.getToken()}`);
121
+ try {
122
+ const tmpDir = path.join(tmpdir(), "shadow-claw");
123
+ if (!fs.existsSync(tmpDir)) {
124
+ fs.mkdirSync(tmpDir, { recursive: true });
125
+ }
126
+ const tokenFilePayload =
127
+ JSON.stringify(
128
+ {
129
+ token: controlPlane.getToken(),
130
+ port: config.port,
131
+ bindHost: config.bindHost,
132
+ createdAt: Date.now(),
133
+ createdAtIso: new Date().toISOString(),
134
+ },
135
+ null,
136
+ 2,
137
+ ) + "\n";
138
+ fs.writeFileSync(
139
+ path.join(tmpDir, `control-token-${config.port}.json`),
140
+ tokenFilePayload,
141
+ "utf8",
142
+ );
143
+ fs.writeFileSync(
144
+ path.join(tmpDir, "control-token.json"),
145
+ tokenFilePayload,
146
+ "utf8",
147
+ );
148
+ } catch (_) {}
119
149
  console.log(
120
150
  `MCP endpoint active at ${protocol}://${config.bindHost}:${config.port}/mcp`,
121
151
  );
@@ -187,6 +187,23 @@ describe("push-client", () => {
187
187
  expect(body.clientId).toBe("client-explicit-123");
188
188
  expect(body.deviceLabel).toBe("Pixel 9 Pro");
189
189
  });
190
+
191
+ it("automatically resolves clientId and deviceLabel if omitted", async () => {
192
+ (fetch as any)
193
+ .mockResolvedValueOnce({
194
+ ok: true,
195
+ json: () => Promise.resolve({ publicKey: "test-vapid-key" }),
196
+ })
197
+ .mockResolvedValueOnce({ ok: true });
198
+
199
+ await subscribeToPush();
200
+ const postCall = (fetch as any).mock.calls[1];
201
+ const body = JSON.parse(postCall[1].body);
202
+ expect(body.clientId).toBeDefined();
203
+ expect(typeof body.clientId).toBe("string");
204
+ expect(body.deviceLabel).toBeDefined();
205
+ expect(typeof body.deviceLabel).toBe("string");
206
+ });
190
207
  });
191
208
 
192
209
  describe("unsubscribeFromPush", () => {
@@ -106,12 +106,32 @@ export async function subscribeToPush(
106
106
  applicationServerKey: urlBase64ToUint8Array(publicKey) as any,
107
107
  });
108
108
 
109
- const resolvedClientId =
109
+ let resolvedClientId =
110
110
  clientId ||
111
111
  (typeof localStorage !== "undefined"
112
112
  ? localStorage.getItem(CONFIG_KEYS.CONTROL_PLANE_CLIENT_ID) || undefined
113
113
  : undefined);
114
114
 
115
+ let resolvedDeviceLabel = deviceLabel;
116
+
117
+ if (!resolvedClientId || !resolvedDeviceLabel) {
118
+ try {
119
+ const initCp = await import("../../core/utils/initControlPlane.js");
120
+ if (
121
+ !resolvedClientId &&
122
+ typeof initCp.getOrCreateControlPlaneClientId === "function"
123
+ ) {
124
+ resolvedClientId = initCp.getOrCreateControlPlaneClientId();
125
+ }
126
+ if (
127
+ !resolvedDeviceLabel &&
128
+ typeof initCp.detectDeviceLabel === "function"
129
+ ) {
130
+ resolvedDeviceLabel = initCp.detectDeviceLabel();
131
+ }
132
+ } catch (_) {}
133
+ }
134
+
115
135
  const subJson =
116
136
  typeof subscription.toJSON === "function"
117
137
  ? subscription.toJSON()
@@ -126,7 +146,7 @@ export async function subscribeToPush(
126
146
  body: JSON.stringify({
127
147
  ...subJson,
128
148
  ...(resolvedClientId ? { clientId: resolvedClientId } : {}),
129
- ...(deviceLabel ? { deviceLabel } : {}),
149
+ ...(resolvedDeviceLabel ? { deviceLabel: resolvedDeviceLabel } : {}),
130
150
  }),
131
151
  }),
132
152
  );
@@ -25,6 +25,7 @@ jest.unstable_mockModule("./push-store.js", () => ({
25
25
  getAllSubscriptions: jest.fn(() => []),
26
26
  getSubscriptionsByClientId: jest.fn(() => []),
27
27
  findSubscriptionsForClient: jest.fn(() => []),
28
+ getRegisteredPushClients: jest.fn(() => []),
28
29
  }));
29
30
 
30
31
  const { registerPushRoutes, broadcastPush } = await import("./push-routes.js");
@@ -179,6 +180,36 @@ describe("push-routes", () => {
179
180
  });
180
181
  });
181
182
 
183
+ describe("GET /push/clients", () => {
184
+ it("registers the route", () => {
185
+ expect(app.routes.get["/push/clients"]).toBeDefined();
186
+ });
187
+
188
+ it("returns registered push clients", async () => {
189
+ (store.getRegisteredPushClients as any).mockReturnValue([
190
+ {
191
+ clientId: "client-abc",
192
+ deviceLabel: "Mac Chrome",
193
+ subscriptionCount: 1,
194
+ lastSeen: "2026-09-01",
195
+ },
196
+ ]);
197
+ const req = createMockReq();
198
+ const res = createMockRes();
199
+ await app.routes.get["/push/clients"](req, res);
200
+ expect(res._json).toEqual({
201
+ clients: [
202
+ {
203
+ clientId: "client-abc",
204
+ deviceLabel: "Mac Chrome",
205
+ subscriptionCount: 1,
206
+ lastSeen: "2026-09-01",
207
+ },
208
+ ],
209
+ });
210
+ });
211
+ });
212
+
182
213
  describe("POST /push/send", () => {
183
214
  it("registers the route", () => {
184
215
  expect(app.routes.post["/push/send"]).toBeDefined();
@@ -16,10 +16,14 @@ import {
16
16
  getSubscription,
17
17
  getAllSubscriptions,
18
18
  findSubscriptionsForClient,
19
+ getRegisteredPushClients,
19
20
  type PushSubscriptionRow,
21
+ type PushClientRecord,
20
22
  } from "./push-store.js";
21
23
  import type { Express } from "express";
22
24
 
25
+ export { getRegisteredPushClients, type PushClientRecord };
26
+
23
27
  export interface BroadcastPushOptions {
24
28
  clientId?: string;
25
29
  }
@@ -159,6 +163,12 @@ export function registerPushRoutes(app: Express): void {
159
163
  res.json(subs);
160
164
  });
161
165
 
166
+ // List unique registered clients with push subscriptions
167
+ app.get("/push/clients", (_req, res) => {
168
+ const clients = getRegisteredPushClients();
169
+ res.json({ clients });
170
+ });
171
+
162
172
  // Send a notification to a specific subscription
163
173
  app.post("/push/send", async (req, res) => {
164
174
  const { endpoint, payload } = req.body || {};
@@ -9,6 +9,7 @@ import {
9
9
  removeSubscriptionById,
10
10
  getSubscriptionsByClientId,
11
11
  findSubscriptionsForClient,
12
+ getRegisteredPushClients,
12
13
  } from "./push-store.js";
13
14
 
14
15
  // Use in-memory DB for tests
@@ -207,4 +208,52 @@ describe("push-store", () => {
207
208
  expect(notFound).toHaveLength(0);
208
209
  });
209
210
  });
211
+
212
+ describe("getRegisteredPushClients", () => {
213
+ it("returns empty array when no subscriptions exist", () => {
214
+ expect(getRegisteredPushClients()).toEqual([]);
215
+ });
216
+
217
+ it("returns deduplicated registered push clients with device labels and counts", () => {
218
+ saveSubscription({
219
+ ...MOCK_SUBSCRIPTION,
220
+ clientId: "client-01jtest123",
221
+ deviceLabel: "Pixel 9 Pro",
222
+ });
223
+ // Second subscription from same client (e.g. renewed endpoint)
224
+ saveSubscription({
225
+ endpoint: "https://fcm.googleapis.com/fcm/send/abc123-renewed",
226
+ keys: MOCK_SUBSCRIPTION.keys,
227
+ clientId: "client-01jtest123",
228
+ deviceLabel: "Pixel 9 Pro",
229
+ });
230
+ // Different client
231
+ saveSubscription({
232
+ ...MOCK_SUBSCRIPTION_2,
233
+ clientId: "client-02ktest456",
234
+ deviceLabel: "MacBook Air",
235
+ });
236
+
237
+ const clients = getRegisteredPushClients();
238
+ expect(clients).toHaveLength(2);
239
+
240
+ const client1 = clients.find((c) => c.clientId === "client-01jtest123");
241
+ expect(client1).toBeDefined();
242
+ expect(client1?.deviceLabel).toBe("Pixel 9 Pro");
243
+ expect(client1?.subscriptionCount).toBe(2);
244
+
245
+ const client2 = clients.find((c) => c.clientId === "client-02ktest456");
246
+ expect(client2).toBeDefined();
247
+ expect(client2?.deviceLabel).toBe("MacBook Air");
248
+ expect(client2?.subscriptionCount).toBe(1);
249
+ });
250
+
251
+ it("falls back to subscription id when clientId is omitted", () => {
252
+ saveSubscription(MOCK_SUBSCRIPTION);
253
+ const clients = getRegisteredPushClients();
254
+ expect(clients).toHaveLength(1);
255
+ const all = getAllSubscriptions();
256
+ expect(clients[0].clientId).toBe(String(all[0].id));
257
+ });
258
+ });
210
259
  });