@hcu-lab.me/mcp-device 1.0.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bootstrap.d.ts +1 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/command-manager.d.ts +8 -0
  6. package/dist/command-manager.js +241 -0
  7. package/dist/config-field-definitions.d.ts +41 -0
  8. package/dist/config-field-definitions.js +37 -0
  9. package/dist/config-manager.d.ts +102 -0
  10. package/dist/config-manager.js +291 -0
  11. package/dist/config.d.ts +6 -0
  12. package/dist/config.js +10 -0
  13. package/dist/custom-stdio.d.ts +51 -0
  14. package/dist/custom-stdio.js +358 -0
  15. package/dist/data/official-app-ca.pem +12 -0
  16. package/dist/data/onboarding-prompts.json +70 -0
  17. package/dist/device/device-state.d.ts +13 -0
  18. package/dist/device/device-state.js +100 -0
  19. package/dist/device/device-status.d.ts +81 -0
  20. package/dist/device/device-status.js +156 -0
  21. package/dist/device/device.d.ts +16 -0
  22. package/dist/device/device.js +190 -0
  23. package/dist/device/execution-engine.d.ts +144 -0
  24. package/dist/device/execution-engine.js +195 -0
  25. package/dist/device/gateway-channel.d.ts +48 -0
  26. package/dist/device/gateway-channel.js +384 -0
  27. package/dist/device/gateway-config.d.ts +40 -0
  28. package/dist/device/gateway-config.js +207 -0
  29. package/dist/device/gateway-identity.d.ts +35 -0
  30. package/dist/device/gateway-identity.js +178 -0
  31. package/dist/device/gateway-pairing.d.ts +25 -0
  32. package/dist/device/gateway-pairing.js +105 -0
  33. package/dist/device/gateway-secure-transport.d.ts +25 -0
  34. package/dist/device/gateway-secure-transport.js +117 -0
  35. package/dist/device/gateway-tool-adapter.d.ts +17 -0
  36. package/dist/device/gateway-tool-adapter.js +284 -0
  37. package/dist/device/gateway-url-policy.d.ts +3 -0
  38. package/dist/device/gateway-url-policy.js +43 -0
  39. package/dist/device/linux-service.d.ts +70 -0
  40. package/dist/device/linux-service.js +268 -0
  41. package/dist/device/official-trust.d.ts +7 -0
  42. package/dist/device/official-trust.js +49 -0
  43. package/dist/device/project-inspection.d.ts +1 -0
  44. package/dist/device/project-inspection.js +180 -0
  45. package/dist/device/runtime-owner.d.ts +41 -0
  46. package/dist/device/runtime-owner.js +177 -0
  47. package/dist/device/windows-dpapi.d.ts +11 -0
  48. package/dist/device/windows-dpapi.js +67 -0
  49. package/dist/device/windows-service.d.ts +56 -0
  50. package/dist/device/windows-service.js +238 -0
  51. package/dist/error-handlers.d.ts +7 -0
  52. package/dist/error-handlers.js +15 -0
  53. package/dist/handlers/edit-search-handlers.d.ts +6 -0
  54. package/dist/handlers/edit-search-handlers.js +6 -0
  55. package/dist/handlers/filesystem-handlers.d.ts +38 -0
  56. package/dist/handlers/filesystem-handlers.js +440 -0
  57. package/dist/handlers/history-handlers.d.ts +12 -0
  58. package/dist/handlers/history-handlers.js +67 -0
  59. package/dist/handlers/index.d.ts +6 -0
  60. package/dist/handlers/index.js +7 -0
  61. package/dist/handlers/process-handlers.d.ts +9 -0
  62. package/dist/handlers/process-handlers.js +15 -0
  63. package/dist/handlers/search-handlers.d.ts +17 -0
  64. package/dist/handlers/search-handlers.js +224 -0
  65. package/dist/handlers/terminal-handlers.d.ts +21 -0
  66. package/dist/handlers/terminal-handlers.js +35 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +138 -0
  69. package/dist/mcp-device.d.ts +2 -0
  70. package/dist/mcp-device.js +7 -0
  71. package/dist/npm-scripts/remote.d.ts +3 -0
  72. package/dist/npm-scripts/remote.js +314 -0
  73. package/dist/search-manager.d.ts +147 -0
  74. package/dist/search-manager.js +828 -0
  75. package/dist/server.d.ts +46 -0
  76. package/dist/server.js +1563 -0
  77. package/dist/terminal-manager.d.ts +104 -0
  78. package/dist/terminal-manager.js +635 -0
  79. package/dist/tools/config.d.ts +96 -0
  80. package/dist/tools/config.js +260 -0
  81. package/dist/tools/edit.d.ts +38 -0
  82. package/dist/tools/edit.js +435 -0
  83. package/dist/tools/feedback.d.ts +8 -0
  84. package/dist/tools/feedback.js +132 -0
  85. package/dist/tools/filesystem.d.ts +79 -0
  86. package/dist/tools/filesystem.js +947 -0
  87. package/dist/tools/fuzzySearch.d.ts +12 -0
  88. package/dist/tools/fuzzySearch.js +74 -0
  89. package/dist/tools/fuzzySearchCore.d.ts +52 -0
  90. package/dist/tools/fuzzySearchCore.js +125 -0
  91. package/dist/tools/improved-process-tools.d.ts +24 -0
  92. package/dist/tools/improved-process-tools.js +627 -0
  93. package/dist/tools/mime-types.d.ts +3 -0
  94. package/dist/tools/mime-types.js +28 -0
  95. package/dist/tools/pdf/extract-images.d.ts +34 -0
  96. package/dist/tools/pdf/extract-images.js +132 -0
  97. package/dist/tools/pdf/index.d.ts +6 -0
  98. package/dist/tools/pdf/index.js +3 -0
  99. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  100. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  101. package/dist/tools/pdf/manipulations.d.ts +13 -0
  102. package/dist/tools/pdf/manipulations.js +99 -0
  103. package/dist/tools/pdf/markdown.d.ts +26 -0
  104. package/dist/tools/pdf/markdown.js +275 -0
  105. package/dist/tools/pdf/utils.d.ts +12 -0
  106. package/dist/tools/pdf/utils.js +34 -0
  107. package/dist/tools/process.d.ts +3 -0
  108. package/dist/tools/process.js +56 -0
  109. package/dist/tools/prompts.d.ts +28 -0
  110. package/dist/tools/prompts.js +259 -0
  111. package/dist/tools/schemas.d.ts +454 -0
  112. package/dist/tools/schemas.js +242 -0
  113. package/dist/tools/usage.d.ts +5 -0
  114. package/dist/tools/usage.js +24 -0
  115. package/dist/types.d.ts +83 -0
  116. package/dist/types.js +1 -0
  117. package/dist/ui/config-editor/config-editor-runtime.js +150 -0
  118. package/dist/ui/config-editor/index.html +13 -0
  119. package/dist/ui/config-editor/src/app.d.ts +43 -0
  120. package/dist/ui/config-editor/src/app.js +831 -0
  121. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  122. package/dist/ui/config-editor/src/array-modal.js +185 -0
  123. package/dist/ui/config-editor/src/main.d.ts +1 -0
  124. package/dist/ui/config-editor/src/main.js +2 -0
  125. package/dist/ui/config-editor/styles.css +587 -0
  126. package/dist/ui/contracts.d.ts +14 -0
  127. package/dist/ui/contracts.js +21 -0
  128. package/dist/ui/file-preview/index.html +16 -0
  129. package/dist/ui/file-preview/preview-runtime.js +469 -0
  130. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  131. package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  132. package/dist/ui/file-preview/src/app.d.ts +4 -0
  133. package/dist/ui/file-preview/src/app.js +680 -0
  134. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  135. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  136. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  137. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  138. package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
  139. package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
  140. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  141. package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
  142. package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
  143. package/dist/ui/file-preview/src/directory-controller.js +240 -0
  144. package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
  145. package/dist/ui/file-preview/src/document-layout.js +109 -0
  146. package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
  147. package/dist/ui/file-preview/src/document-outline.js +97 -0
  148. package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
  149. package/dist/ui/file-preview/src/document-workspace.js +33 -0
  150. package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
  151. package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
  152. package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
  153. package/dist/ui/file-preview/src/host/external-actions.js +55 -0
  154. package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
  155. package/dist/ui/file-preview/src/host/selection-context.js +106 -0
  156. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  157. package/dist/ui/file-preview/src/image-preview.js +21 -0
  158. package/dist/ui/file-preview/src/main.d.ts +1 -0
  159. package/dist/ui/file-preview/src/main.js +5 -0
  160. package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
  161. package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
  162. package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
  163. package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
  164. package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
  165. package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
  166. package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
  167. package/dist/ui/file-preview/src/markdown/linking.js +228 -0
  168. package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
  169. package/dist/ui/file-preview/src/markdown/outline.js +16 -0
  170. package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
  171. package/dist/ui/file-preview/src/markdown/parser.js +38 -0
  172. package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
  173. package/dist/ui/file-preview/src/markdown/preview.js +20 -0
  174. package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
  175. package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
  176. package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
  177. package/dist/ui/file-preview/src/markdown/utils.js +15 -0
  178. package/dist/ui/file-preview/src/model.d.ts +35 -0
  179. package/dist/ui/file-preview/src/model.js +1 -0
  180. package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
  181. package/dist/ui/file-preview/src/panel-actions.js +182 -0
  182. package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
  183. package/dist/ui/file-preview/src/path-utils.js +64 -0
  184. package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
  185. package/dist/ui/file-preview/src/payload-utils.js +119 -0
  186. package/dist/ui/file-preview/src/types.d.ts +1 -0
  187. package/dist/ui/file-preview/src/types.js +1 -0
  188. package/dist/ui/file-preview/styles.css +1631 -0
  189. package/dist/ui/resources.d.ts +28 -0
  190. package/dist/ui/resources.js +86 -0
  191. package/dist/ui/shared/compact-row.d.ts +11 -0
  192. package/dist/ui/shared/compact-row.js +18 -0
  193. package/dist/ui/shared/escape-html.d.ts +4 -0
  194. package/dist/ui/shared/escape-html.js +11 -0
  195. package/dist/ui/shared/host-context.d.ts +15 -0
  196. package/dist/ui/shared/host-context.js +51 -0
  197. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  198. package/dist/ui/shared/tool-bridge.js +137 -0
  199. package/dist/ui/shared/tool-shell.d.ts +25 -0
  200. package/dist/ui/shared/tool-shell.js +107 -0
  201. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  202. package/dist/ui/shared/ui-event-tracker.js +27 -0
  203. package/dist/ui/shared/widget-state.d.ts +33 -0
  204. package/dist/ui/shared/widget-state.js +158 -0
  205. package/dist/utils/ab-test.d.ts +12 -0
  206. package/dist/utils/ab-test.js +104 -0
  207. package/dist/utils/capture.d.ts +35 -0
  208. package/dist/utils/capture.js +481 -0
  209. package/dist/utils/dockerPrompt.d.ts +17 -0
  210. package/dist/utils/dockerPrompt.js +53 -0
  211. package/dist/utils/feature-flags.d.ts +59 -0
  212. package/dist/utils/feature-flags.js +215 -0
  213. package/dist/utils/files/base.d.ts +173 -0
  214. package/dist/utils/files/base.js +5 -0
  215. package/dist/utils/files/binary.d.ts +21 -0
  216. package/dist/utils/files/binary.js +65 -0
  217. package/dist/utils/files/docx.d.ts +47 -0
  218. package/dist/utils/files/docx.js +679 -0
  219. package/dist/utils/files/excel.d.ts +24 -0
  220. package/dist/utils/files/excel.js +437 -0
  221. package/dist/utils/files/factory.d.ts +41 -0
  222. package/dist/utils/files/factory.js +113 -0
  223. package/dist/utils/files/image.d.ts +21 -0
  224. package/dist/utils/files/image.js +78 -0
  225. package/dist/utils/files/index.d.ts +10 -0
  226. package/dist/utils/files/index.js +13 -0
  227. package/dist/utils/files/pdf.d.ts +32 -0
  228. package/dist/utils/files/pdf.js +142 -0
  229. package/dist/utils/files/text.d.ts +63 -0
  230. package/dist/utils/files/text.js +370 -0
  231. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  232. package/dist/utils/fuzzySearchLogger.js +126 -0
  233. package/dist/utils/lineEndingHandler.d.ts +21 -0
  234. package/dist/utils/lineEndingHandler.js +77 -0
  235. package/dist/utils/logger.d.ts +32 -0
  236. package/dist/utils/logger.js +72 -0
  237. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  238. package/dist/utils/mcp-ui-ab-test.js +62 -0
  239. package/dist/utils/open-browser.d.ts +9 -0
  240. package/dist/utils/open-browser.js +42 -0
  241. package/dist/utils/process-detection.d.ts +23 -0
  242. package/dist/utils/process-detection.js +150 -0
  243. package/dist/utils/ripgrep-resolver.d.ts +9 -0
  244. package/dist/utils/ripgrep-resolver.js +74 -0
  245. package/dist/utils/system-info.d.ts +71 -0
  246. package/dist/utils/system-info.js +712 -0
  247. package/dist/utils/toolHistory.d.ts +86 -0
  248. package/dist/utils/toolHistory.js +257 -0
  249. package/dist/utils/trackTools.d.ts +6 -0
  250. package/dist/utils/trackTools.js +54 -0
  251. package/dist/utils/unsupportedParams.d.ts +24 -0
  252. package/dist/utils/unsupportedParams.js +66 -0
  253. package/dist/utils/usageTracker.d.ts +133 -0
  254. package/dist/utils/usageTracker.js +477 -0
  255. package/dist/utils/welcome-onboarding.d.ts +11 -0
  256. package/dist/utils/welcome-onboarding.js +99 -0
  257. package/dist/utils/withTimeout.d.ts +28 -0
  258. package/dist/utils/withTimeout.js +85 -0
  259. package/dist/version.d.ts +1 -0
  260. package/dist/version.js +1 -0
  261. package/package.json +131 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Eduard Ruzga and Desktop Commander Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # MCP Device
2
+
3
+ `@hcu-lab.me/mcp-device` securely connects a user-owned computer to an authenticated Model Context Protocol gateway. The device makes the outbound connection; no inbound device port is required.
4
+
5
+ The gateway owns accounts, OAuth, device ownership, MCP tool contracts, routing, and usage aggregation. MCP Device owns local device identity, pairing, reconnect lifecycle, bounded execution, background registration, and local status.
6
+
7
+ ## Commands
8
+
9
+ ```text
10
+ mcp-device start in the foreground
11
+ mcp-device start start in the foreground
12
+ mcp-device login explicitly link or switch the gateway account
13
+ mcp-device logout unlink the gateway account from this device
14
+ mcp-device status show runtime, manager, connection, security, proxy, usage, and schema state
15
+ mcp-device install pair if needed, then install and start a background runtime
16
+ mcp-device stop stop the current runtime without deleting identity/account state
17
+ mcp-device uninstall remove background registration while preserving identity/account state
18
+ mcp-device --help show command help
19
+ ```
20
+
21
+ `md` is an alias of `mcp-device` on POSIX shells. On Windows, use `mcp-device`; bare `md` has a shell collision with built-in directory-creation commands/aliases and is not the supported invocation.
22
+
23
+ On Windows, `install` uses the current-user background registration. On Linux, every `install` asks whether to use `systemd --user` or an already configured PM2 installation. MCP Device does not install PM2, run `pm2 startup`, or use `sudo` automatically.
24
+
25
+ ## First run and trust
26
+
27
+ The official gateway is `https://mcp-v2.hcu-lab.me/mcp`. The official package is designed to pin an independently distributed application CA and require protocol v2 before the first network connection. If that CA is absent, MCP Device fails closed rather than learning trust from the gateway it is about to contact or falling back to protocol v1.
28
+
29
+ For a custom gateway, configure the gateway URL and provision its application CA through an independent trusted channel before enabling protocol v2. Proxy selection is captured during interactive provisioning and reused by the background runtime; proxy credentials are protected with Windows DPAPI on Windows.
30
+
31
+ ## Local state and migration
32
+
33
+ Canonical state is stored under `~/.mcp-device/`. Existing `~/.hcu-device/` identity/config/status state is migrated non-destructively under serialized runtime ownership. A conflicting canonical and legacy identity fails closed rather than creating a second device identity.
34
+
35
+ The device identity is Ed25519. On Windows, the private key is protected with DPAPI for the current user. The gateway stores the public identity and account ownership.
36
+
37
+ ## Development
38
+
39
+ ```powershell
40
+ git clone https://github.com/Hiroshimeow/agent-mcp-device.git
41
+ cd agent-mcp-device
42
+ npm ci
43
+ npm run build
44
+ node dist/mcp-device.js --help
45
+ ```
46
+
47
+ For a custom development gateway, set `MCP_GATEWAY_URL`. Supply `MCP_GATEWAY_APP_CA_PATH` from an independent trusted source when using protocol v2. Non-loopback plaintext gateway URLs are rejected.
48
+
49
+ ## Upstream execution engine
50
+
51
+ The local execution engine is derived from [Desktop Commander MCP](https://github.com/wonderwhy-er/DesktopCommanderMCP). Desktop Commander is MIT licensed; its copyright and MIT license are preserved in this repository's `LICENSE`. The MCP Device product identity, gateway protocol, account/device lifecycle, security layer, and packaging are maintained separately in this repository.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Threadpool bootstrap. MUST be the first import in index.ts.
3
+ *
4
+ * Every fs operation (read_file, write_file, edit_block, config/history/log
5
+ * persistence) runs on libuv's threadpool, which defaults to only 4 threads.
6
+ * Under heavy parallel load — e.g. several agents reading/writing files on a
7
+ * slow or cloud-synced filesystem — 4 stalled operations exhaust the pool, and
8
+ * because a stalled syscall keeps its thread until the OS returns (a JS-level
9
+ * timeout does not cancel it), every subsequent fs op queues for minutes. That
10
+ * surfaced as multi-minute tool-call hangs under parallel `claude -p` load.
11
+ *
12
+ * Raising the pool size gives enough headroom that a burst of slow reads no
13
+ * longer starves the rest. libuv reads UV_THREADPOOL_SIZE only when the pool is
14
+ * first initialized (on first submitted work), so this assignment has to happen
15
+ * before ANY threadpool work — hence "first import". A user-provided value is
16
+ * always respected.
17
+ */
18
+ const DEFAULT_THREADPOOL_SIZE = 16;
19
+ if (!process.env.UV_THREADPOOL_SIZE) {
20
+ process.env.UV_THREADPOOL_SIZE = String(DEFAULT_THREADPOOL_SIZE);
21
+ }
22
+ export {};
@@ -0,0 +1,8 @@
1
+ declare class CommandManager {
2
+ getBaseCommand(command: string): string;
3
+ extractCommands(commandString: string): string[];
4
+ extractBaseCommand(commandStr: string): string | null;
5
+ validateCommand(command: string): Promise<boolean>;
6
+ }
7
+ export declare const commandManager: CommandManager;
8
+ export {};
@@ -0,0 +1,241 @@
1
+ import path from 'path';
2
+ import { configManager } from './config-manager.js';
3
+ import { capture } from "./utils/capture.js";
4
+ class CommandManager {
5
+ getBaseCommand(command) {
6
+ return command.split(' ')[0].toLowerCase().trim();
7
+ }
8
+ extractCommands(commandString) {
9
+ try {
10
+ // Trim any leading/trailing whitespace
11
+ commandString = commandString.trim();
12
+ // Define command separators - these are the operators that can chain commands
13
+ const separators = [';', '&&', '||', '|', '&'];
14
+ // This will store our extracted commands
15
+ const commands = [];
16
+ // Split by common separators while preserving quotes
17
+ let inQuote = false;
18
+ let quoteChar = '';
19
+ let currentCmd = '';
20
+ let escaped = false;
21
+ for (let i = 0; i < commandString.length; i++) {
22
+ const char = commandString[i];
23
+ // Handle escape characters
24
+ if (char === '\\' && !escaped) {
25
+ escaped = true;
26
+ currentCmd += char;
27
+ continue;
28
+ }
29
+ // If this character is escaped, just add it
30
+ if (escaped) {
31
+ escaped = false;
32
+ currentCmd += char;
33
+ continue;
34
+ }
35
+ // Handle quotes (both single and double)
36
+ if ((char === '"' || char === "'") && !inQuote) {
37
+ inQuote = true;
38
+ quoteChar = char;
39
+ currentCmd += char;
40
+ continue;
41
+ }
42
+ else if (char === quoteChar && inQuote) {
43
+ inQuote = false;
44
+ quoteChar = '';
45
+ currentCmd += char;
46
+ continue;
47
+ }
48
+ // Handle $() command substitution even inside quotes (fixes blocklist bypass)
49
+ if (char === '$' && i + 1 < commandString.length && commandString[i + 1] === '(') {
50
+ const startIndex = i;
51
+ let openParens = 1;
52
+ let j = i + 2; // skip past $(
53
+ while (j < commandString.length && openParens > 0) {
54
+ if (commandString[j] === '(')
55
+ openParens++;
56
+ if (commandString[j] === ')')
57
+ openParens--;
58
+ j++;
59
+ }
60
+ if (j <= commandString.length && openParens === 0) {
61
+ const subContent = commandString.substring(i + 2, j - 1);
62
+ const subCommands = this.extractCommands(subContent);
63
+ commands.push(...subCommands);
64
+ i = j - 1;
65
+ if (!inQuote) {
66
+ continue;
67
+ }
68
+ else {
69
+ currentCmd += commandString.substring(startIndex, j);
70
+ continue;
71
+ }
72
+ }
73
+ }
74
+ // Handle backtick command substitution even inside quotes
75
+ if (char === '`') {
76
+ const startIndex = i;
77
+ let j = i + 1;
78
+ while (j < commandString.length && commandString[j] !== '`') {
79
+ j++;
80
+ }
81
+ if (j < commandString.length) {
82
+ const subContent = commandString.substring(i + 1, j);
83
+ const subCommands = this.extractCommands(subContent);
84
+ commands.push(...subCommands);
85
+ i = j;
86
+ if (!inQuote) {
87
+ continue;
88
+ }
89
+ else {
90
+ currentCmd += commandString.substring(startIndex, j + 1);
91
+ continue;
92
+ }
93
+ }
94
+ }
95
+ // If we're inside quotes, just add the character
96
+ if (inQuote) {
97
+ currentCmd += char;
98
+ continue;
99
+ }
100
+ // Handle subshells - if we see an opening parenthesis, we need to find its matching closing parenthesis
101
+ if (char === '(') {
102
+ // Find the matching closing parenthesis
103
+ let openParens = 1;
104
+ let j = i + 1;
105
+ while (j < commandString.length && openParens > 0) {
106
+ if (commandString[j] === '(')
107
+ openParens++;
108
+ if (commandString[j] === ')')
109
+ openParens--;
110
+ j++;
111
+ }
112
+ // Skip to after the closing parenthesis only if properly balanced
113
+ if (j <= commandString.length && openParens === 0) {
114
+ const subshellContent = commandString.substring(i + 1, j - 1);
115
+ // Recursively extract commands from the subshell
116
+ const subCommands = this.extractCommands(subshellContent);
117
+ commands.push(...subCommands);
118
+ // Move position past the subshell
119
+ i = j - 1;
120
+ continue;
121
+ }
122
+ }
123
+ // Check for separators
124
+ let isSeparator = false;
125
+ for (const separator of separators) {
126
+ if (commandString.startsWith(separator, i)) {
127
+ // We found a separator - extract the command before it
128
+ if (currentCmd.trim()) {
129
+ const baseCommand = this.extractBaseCommand(currentCmd.trim());
130
+ if (baseCommand)
131
+ commands.push(baseCommand);
132
+ }
133
+ // Move past the separator
134
+ i += separator.length - 1;
135
+ currentCmd = '';
136
+ isSeparator = true;
137
+ break;
138
+ }
139
+ }
140
+ if (!isSeparator) {
141
+ currentCmd += char;
142
+ }
143
+ }
144
+ // Don't forget to add the last command
145
+ if (currentCmd.trim()) {
146
+ const baseCommand = this.extractBaseCommand(currentCmd.trim());
147
+ if (baseCommand)
148
+ commands.push(baseCommand);
149
+ }
150
+ // Remove duplicates and return
151
+ return [...new Set(commands)];
152
+ }
153
+ catch (error) {
154
+ // If anything goes wrong, log the error but return the basic command to not break execution
155
+ capture('server_request_error', {
156
+ error: 'Error extracting commands'
157
+ });
158
+ const baseCmd = this.extractBaseCommand(commandString);
159
+ return baseCmd ? [baseCmd] : [];
160
+ }
161
+ }
162
+ // This extracts the actual command name from a command string
163
+ extractBaseCommand(commandStr) {
164
+ try {
165
+ // Remove environment variables (patterns like KEY=value)
166
+ const withoutEnvVars = commandStr.replace(/\w+=\S+\s*/g, '').trim();
167
+ // If nothing remains after removing env vars, return null
168
+ if (!withoutEnvVars)
169
+ return null;
170
+ // Get the first token (the command)
171
+ const tokens = withoutEnvVars.split(/\s+/);
172
+ let firstToken = null;
173
+ // Find the first valid token (skip variables)
174
+ for (let i = 0; i < tokens.length; i++) {
175
+ const token = tokens[i];
176
+ // Skip dollar-prefixed tokens (variables) but not $() command substitutions
177
+ if (token.startsWith('$') && !token.startsWith('$(')) {
178
+ continue;
179
+ }
180
+ // Check if it starts with special characters like ( that might indicate it's not a regular command
181
+ if (token[0] === '(') {
182
+ continue;
183
+ }
184
+ firstToken = token;
185
+ break;
186
+ }
187
+ // No valid command token found
188
+ if (!firstToken) {
189
+ return null;
190
+ }
191
+ // handle $() command substitution - extract the inner command
192
+ if (firstToken.startsWith('$(') && firstToken.endsWith(')')) {
193
+ const inner = firstToken.slice(2, -1).trim();
194
+ if (inner) {
195
+ const innerTokens = inner.split(/\s+/);
196
+ return path.basename(innerTokens[0]).toLowerCase();
197
+ }
198
+ return null;
199
+ }
200
+ // strip path prefix so /usr/bin/sudo gets caught as "sudo"
201
+ const baseName = path.basename(firstToken);
202
+ return baseName.toLowerCase();
203
+ }
204
+ catch (error) {
205
+ capture('Error extracting base command');
206
+ return null;
207
+ }
208
+ }
209
+ async validateCommand(command) {
210
+ try {
211
+ // Get blocked commands from config
212
+ const config = await configManager.getConfig();
213
+ const blockedCommands = config.blockedCommands || [];
214
+ // Extract all commands from the command string
215
+ const allCommands = this.extractCommands(command);
216
+ // If there are no commands extracted, fall back to base command
217
+ if (allCommands.length === 0) {
218
+ const baseCommand = this.getBaseCommand(command);
219
+ return !blockedCommands.includes(baseCommand);
220
+ }
221
+ // Check if any of the extracted commands are in the blocked list
222
+ for (const cmd of allCommands) {
223
+ if (blockedCommands.includes(cmd)) {
224
+ return false; // Command is blocked
225
+ }
226
+ }
227
+ // No commands were blocked
228
+ return true;
229
+ }
230
+ catch (error) {
231
+ console.error('Error validating command:', error);
232
+ capture('server_validate_command_error', {
233
+ error: error instanceof Error ? error.message : String(error)
234
+ });
235
+ // Fail closed: deny the command if validation encounters an error.
236
+ // This prevents a config read failure from bypassing all command filtering.
237
+ return false;
238
+ }
239
+ }
240
+ }
241
+ export const commandManager = new CommandManager();
@@ -0,0 +1,41 @@
1
+ export type ConfigFieldValueType = 'string' | 'number' | 'boolean' | 'array' | 'null';
2
+ export type ConfigFieldDefinition = {
3
+ label: string;
4
+ description: string;
5
+ valueType: ConfigFieldValueType;
6
+ };
7
+ export declare const CONFIG_FIELD_DEFINITIONS: {
8
+ readonly blockedCommands: {
9
+ readonly label: "Blocked Commands";
10
+ readonly description: "This is your personal safety blocklist. If a command appears here, MCP Device will refuse to run it even if a prompt asks for it. Add risky commands you never want executed by mistake.";
11
+ readonly valueType: "array";
12
+ };
13
+ readonly allowedDirectories: {
14
+ readonly label: "Allowed Folders";
15
+ readonly description: "These are the folders MCP Device is allowed to read and edit. Think of this as a permission list. Keeping it small is safer. If this list is empty, MCP Device can access your entire filesystem.";
16
+ readonly valueType: "array";
17
+ };
18
+ readonly defaultShell: {
19
+ readonly label: "Default Shell";
20
+ readonly description: "This is the shell used for new command sessions (for example /bin/bash or /bin/zsh). Only change this if you know your environment requires a specific shell.";
21
+ readonly valueType: "string";
22
+ };
23
+ readonly telemetryEnabled: {
24
+ readonly label: "Anonymous Telemetry";
25
+ readonly description: "When on, MCP Device sends anonymous usage information that helps improve product quality. When off, no telemetry data is sent.";
26
+ readonly valueType: "boolean";
27
+ };
28
+ readonly fileReadLineLimit: {
29
+ readonly label: "File Read Limit";
30
+ readonly description: "Maximum number of lines returned from a file in one read action. Lower numbers keep responses short and safer; higher numbers return more text at once.";
31
+ readonly valueType: "number";
32
+ };
33
+ readonly fileWriteLineLimit: {
34
+ readonly label: "File Write Limit";
35
+ readonly description: "Maximum number of lines that can be written in one edit operation. This helps prevent accidental oversized writes and keeps file changes predictable.";
36
+ readonly valueType: "number";
37
+ };
38
+ };
39
+ export type ConfigFieldKey = keyof typeof CONFIG_FIELD_DEFINITIONS;
40
+ export declare const CONFIG_FIELD_KEYS: ConfigFieldKey[];
41
+ export declare function isConfigFieldKey(value: string): value is ConfigFieldKey;
@@ -0,0 +1,37 @@
1
+ // Single source of truth for user-editable configuration fields.
2
+ export const CONFIG_FIELD_DEFINITIONS = {
3
+ blockedCommands: {
4
+ label: 'Blocked Commands',
5
+ description: 'This is your personal safety blocklist. If a command appears here, MCP Device will refuse to run it even if a prompt asks for it. Add risky commands you never want executed by mistake.',
6
+ valueType: 'array',
7
+ },
8
+ allowedDirectories: {
9
+ label: 'Allowed Folders',
10
+ description: 'These are the folders MCP Device is allowed to read and edit. Think of this as a permission list. Keeping it small is safer. If this list is empty, MCP Device can access your entire filesystem.',
11
+ valueType: 'array',
12
+ },
13
+ defaultShell: {
14
+ label: 'Default Shell',
15
+ description: 'This is the shell used for new command sessions (for example /bin/bash or /bin/zsh). Only change this if you know your environment requires a specific shell.',
16
+ valueType: 'string',
17
+ },
18
+ telemetryEnabled: {
19
+ label: 'Anonymous Telemetry',
20
+ description: 'When on, MCP Device sends anonymous usage information that helps improve product quality. When off, no telemetry data is sent.',
21
+ valueType: 'boolean',
22
+ },
23
+ fileReadLineLimit: {
24
+ label: 'File Read Limit',
25
+ description: 'Maximum number of lines returned from a file in one read action. Lower numbers keep responses short and safer; higher numbers return more text at once.',
26
+ valueType: 'number',
27
+ },
28
+ fileWriteLineLimit: {
29
+ label: 'File Write Limit',
30
+ description: 'Maximum number of lines that can be written in one edit operation. This helps prevent accidental oversized writes and keeps file changes predictable.',
31
+ valueType: 'number',
32
+ },
33
+ };
34
+ export const CONFIG_FIELD_KEYS = Object.keys(CONFIG_FIELD_DEFINITIONS);
35
+ export function isConfigFieldKey(value) {
36
+ return Object.prototype.hasOwnProperty.call(CONFIG_FIELD_DEFINITIONS, value);
37
+ }
@@ -0,0 +1,102 @@
1
+ export interface ServerConfig {
2
+ blockedCommands?: string[];
3
+ defaultShell?: string;
4
+ allowedDirectories?: string[];
5
+ telemetryEnabled?: boolean;
6
+ fileWriteLineLimit?: number;
7
+ fileReadLineLimit?: number;
8
+ clientId?: string;
9
+ currentClient?: ClientInfo;
10
+ [key: string]: any;
11
+ }
12
+ export interface ClientInfo {
13
+ name: string;
14
+ version: string;
15
+ }
16
+ export declare function normalizeTelemetryEnabledValue(value: unknown): unknown;
17
+ export declare function isTelemetryDisabledValue(value: unknown): boolean;
18
+ /**
19
+ * Singleton config manager for the server
20
+ */
21
+ declare class ConfigManager {
22
+ private configPath;
23
+ private config;
24
+ private initialized;
25
+ private _isFirstRun;
26
+ private writeChain;
27
+ private saveScheduled;
28
+ constructor();
29
+ /**
30
+ * Initialize configuration - load from disk or create default
31
+ */
32
+ init(): Promise<void>;
33
+ /**
34
+ * Alias for init() to maintain backward compatibility
35
+ */
36
+ loadConfig(): Promise<void>;
37
+ /**
38
+ * Create default configuration
39
+ */
40
+ private getDefaultConfig;
41
+ /**
42
+ * Write the current in-memory config to disk. All writes funnel through
43
+ * writeChain (see saveConfig / scheduleSave) so overlapping saves can never
44
+ * interleave and corrupt the file. Previously every tool call could fire its
45
+ * own independent fs.writeFile of the same path.
46
+ */
47
+ private writeConfigToDisk;
48
+ /**
49
+ * Awaitable save, serialized on writeChain. Use for explicit, user-driven
50
+ * config changes where the caller wants on-disk confirmation.
51
+ */
52
+ private saveConfig;
53
+ /**
54
+ * Non-blocking, coalesced save. Returns immediately; the write runs in the
55
+ * background. A burst of calls collapses to at most one queued write behind
56
+ * the in-flight one, so a storm of tool calls can't storm the disk — and,
57
+ * critically, can't pile up behind a saturated libuv threadpool and gate the
58
+ * tool-call response path. Used for high-frequency, non-critical persistence
59
+ * such as usage stats.
60
+ */
61
+ scheduleSave(): void;
62
+ /**
63
+ * Get the entire config
64
+ */
65
+ getConfig(): Promise<ServerConfig>;
66
+ /**
67
+ * Get a specific configuration value
68
+ */
69
+ getValue(key: string): Promise<any>;
70
+ /**
71
+ * Set a specific configuration value
72
+ */
73
+ setValue(key: string, value: any): Promise<void>;
74
+ /**
75
+ * Update a value in memory and persist it WITHOUT blocking the caller.
76
+ * The tool-call response path must never wait on a disk write: when the libuv
77
+ * threadpool is saturated (e.g. many parallel reads stalled on a slow/cloud
78
+ * filesystem) an awaited write can't get a thread and would hang the response
79
+ * of even pure-memory tools. The in-memory value is updated synchronously so
80
+ * subsequent reads see it immediately; the write is coalesced in the
81
+ * background. Callers needing on-disk confirmation should use setValue.
82
+ */
83
+ setValueNonBlocking(key: string, value: any): Promise<void>;
84
+ /**
85
+ * Update multiple configuration values at once
86
+ */
87
+ updateConfig(updates: Partial<ServerConfig>): Promise<ServerConfig>;
88
+ /**
89
+ * Reset configuration to defaults
90
+ */
91
+ resetConfig(): Promise<ServerConfig>;
92
+ /**
93
+ * Check if this is the first run (config file was just created)
94
+ */
95
+ isFirstRun(): boolean;
96
+ /**
97
+ * Get or create a persistent client ID for analytics and A/B tests
98
+ */
99
+ getOrCreateClientId(): Promise<string>;
100
+ }
101
+ export declare const configManager: ConfigManager;
102
+ export {};