@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- 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 {};
|