@hcu-lab.me/mcp-device 1.0.5 → 1.0.7
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/README.md +7 -71
- package/dist/bootstrap.js +1 -1
- package/dist/config-manager.js +56 -19
- package/dist/config.js +1 -1
- package/dist/custom-stdio.js +1 -2
- package/dist/data/onboarding-prompts.json +22 -11
- package/dist/device/device-state.d.ts +2 -5
- package/dist/device/device-state.js +3 -85
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +69 -9
- package/dist/device/execution-engine.d.ts +11 -1
- package/dist/device/execution-engine.js +85 -126
- package/dist/device/gateway-channel.d.ts +19 -1
- package/dist/device/gateway-channel.js +107 -21
- package/dist/device/gateway-identity.d.ts +1 -0
- package/dist/device/gateway-identity.js +4 -0
- package/dist/device/gateway-tool-adapter.d.ts +3 -3
- package/dist/device/gateway-tool-adapter.js +12 -11
- package/dist/device/linux-service.d.ts +6 -0
- package/dist/device/linux-service.js +12 -5
- package/dist/device/runtime-owner.d.ts +1 -0
- package/dist/device/runtime-owner.js +6 -6
- package/dist/device/self-update.d.ts +107 -12
- package/dist/device/self-update.js +660 -62
- package/dist/device/update-helper.cjs +343 -0
- package/dist/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- package/dist/handlers/filesystem-handlers.js +3 -0
- package/dist/index.js +7 -3
- package/dist/npm-scripts/remote.d.ts +5 -0
- package/dist/npm-scripts/remote.js +50 -3
- package/dist/server.js +83 -167
- package/dist/terminal-manager.js +41 -1
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/filesystem.js +3 -3
- package/dist/tools/pdf/markdown.js +1 -1
- package/dist/tools/schemas.d.ts +2 -3
- package/dist/tools/schemas.js +0 -10
- package/dist/ui/config-editor/index.html +1 -1
- package/dist/ui/file-preview/index.html +1 -1
- package/dist/ui/file-preview/styles.css +2 -2
- package/dist/utils/capture.d.ts +5 -28
- package/dist/utils/capture.js +11 -455
- package/dist/utils/feature-flags.d.ts +0 -47
- package/dist/utils/feature-flags.js +5 -199
- package/dist/utils/files/docx.d.ts +2 -2
- package/dist/utils/files/docx.js +2 -2
- package/dist/utils/files/factory.js +20 -0
- package/dist/utils/files/text.d.ts +1 -0
- package/dist/utils/files/text.js +78 -34
- package/dist/utils/trackTools.js +23 -2
- package/dist/utils/usageTracker.js +2 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/feedback.d.ts +0 -8
- package/dist/tools/feedback.js +0 -132
package/dist/server.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
2
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
3
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
@@ -9,11 +8,10 @@ const OS_GUIDANCE = getOSSpecificGuidance(SYSTEM_INFO);
|
|
|
9
8
|
const DEV_TOOL_GUIDANCE = getDevelopmentToolGuidance(SYSTEM_INFO);
|
|
10
9
|
const PATH_GUIDANCE = `IMPORTANT: ${getPathGuidance(SYSTEM_INFO)} Relative paths may fail as they depend on the current working directory. Tilde paths (~/...) might not work in all contexts. Unless the user explicitly asks for relative paths, use absolute paths.`;
|
|
11
10
|
const CMD_PREFIX_DESCRIPTION = `This command can be referenced as "MCP Device: ..." or "use MCP Device to ..." in your instructions.`;
|
|
12
|
-
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema,
|
|
11
|
+
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, StartSearchArgsSchema, GetMoreSearchResultsArgsSchema, StopSearchArgsSchema, ListSearchesArgsSchema, GetPromptsArgsSchema, GetRecentToolCallsArgsSchema, WritePdfArgsSchema, toolArgSchemas, } from './tools/schemas.js';
|
|
13
12
|
import { detectUnsupportedParams, getSupportedParams, buildUnsupportedParamsWarning, } from './utils/unsupportedParams.js';
|
|
14
13
|
import { getConfig, setConfigValue } from './tools/config.js';
|
|
15
14
|
import { getUsageStats } from './tools/usage.js';
|
|
16
|
-
import { giveFeedbackToDesktopCommander } from './tools/feedback.js';
|
|
17
15
|
import { getPrompts } from './tools/prompts.js';
|
|
18
16
|
import { trackToolCall } from './utils/trackTools.js';
|
|
19
17
|
import { usageTracker } from './utils/usageTracker.js';
|
|
@@ -89,7 +87,7 @@ function setCurrentCallIsRemote(isRemote) {
|
|
|
89
87
|
currentCallIsRemote = isRemote;
|
|
90
88
|
}
|
|
91
89
|
// The remote caller's client for the in-flight tool call (e.g. openai-mcp,
|
|
92
|
-
//
|
|
90
|
+
// remote client). Set per CallTool when the call is remote; null for local calls.
|
|
93
91
|
// Mirrors currentCallIsRemote so telemetry attributes remote events to the
|
|
94
92
|
// actual remote client instead of the device's own currentClient (which stays
|
|
95
93
|
// LOCAL and must not be polluted by remote callers).
|
|
@@ -104,11 +102,10 @@ function setCurrentRemoteClient(clientInfo) {
|
|
|
104
102
|
/**
|
|
105
103
|
* True when this server instance is serving remote services rather than a
|
|
106
104
|
* local MCP client. The device wrapper marks the server it spawns with
|
|
107
|
-
* MCP_DEVICE_REMOTE=true (see device/execution-engine.ts)
|
|
108
|
-
* the client-name check covers older wrappers that predate the env marker.
|
|
105
|
+
* MCP_DEVICE_REMOTE=true (see device/execution-engine.ts).
|
|
109
106
|
*/
|
|
110
107
|
function isRemoteClientContext(clientName) {
|
|
111
|
-
return process.env.MCP_DEVICE_REMOTE === 'true'
|
|
108
|
+
return process.env.MCP_DEVICE_REMOTE === 'true';
|
|
112
109
|
}
|
|
113
110
|
/**
|
|
114
111
|
* Unified way to update client information
|
|
@@ -138,40 +135,19 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
138
135
|
const clientInfo = request.params?.clientInfo;
|
|
139
136
|
if (clientInfo) {
|
|
140
137
|
await updateCurrentClient(clientInfo);
|
|
141
|
-
|
|
142
|
-
// the Desktop Commander app and remote contexts. Further exclusions are
|
|
143
|
-
// flag-served via welcome_page_excluded_clients (e.g. claude-code, which
|
|
144
|
-
// covers Claude Code and Cowork plugin sessions — both identify as
|
|
145
|
-
// `claude-code` and provide their own onboarding surface).
|
|
146
|
-
const isWelcomePageEligibleClient = currentClient.name !== 'desktop-commander-app'
|
|
147
|
-
&& currentClient.name !== 'desktop-commander'
|
|
148
|
-
&& !isRemoteClientContext(currentClient.name)
|
|
138
|
+
const isWelcomePageEligibleClient = !isRemoteClientContext(currentClient.name)
|
|
149
139
|
&& !global.disableOnboarding;
|
|
150
|
-
if (
|
|
140
|
+
if (isRemoteClientContext(currentClient.name)) {
|
|
141
|
+
// Remote child engines do not mutate local onboarding state.
|
|
142
|
+
}
|
|
143
|
+
else if (isWelcomePageEligibleClient) {
|
|
151
144
|
await handleWelcomePageOnboarding(currentClient.name);
|
|
152
145
|
}
|
|
153
146
|
else {
|
|
154
|
-
// Do not carry a first-run page over to a client that is made
|
|
155
|
-
// eligible in a later release.
|
|
156
147
|
await skipWelcomePageOnboarding();
|
|
157
148
|
}
|
|
149
|
+
capture('run_server_mcp_initialized');
|
|
158
150
|
}
|
|
159
|
-
// Raw host environment signals (no PII, undefined when absent). Some
|
|
160
|
-
// hosts share a clientInfo name — Claude Code CLI, Claude Code inside
|
|
161
|
-
// the Claude Desktop app, and Cowork all report 'claude-code' — and
|
|
162
|
-
// these let analytics tell them apart without client-specific
|
|
163
|
-
// branching in code. Verified signatures: CLI → entrypoint 'cli';
|
|
164
|
-
// CC-in-desktop → entrypoint 'claude-desktop'; Cowork → no
|
|
165
|
-
// entrypoint/agent, plugin id 'desktop-commander-inline'.
|
|
166
|
-
// Values truncated to GA4's 100-char param limit (same convention as
|
|
167
|
-
// containerName/containerImage) so an oversized value can never get
|
|
168
|
-
// the whole event rejected.
|
|
169
|
-
capture('run_server_mcp_initialized', {
|
|
170
|
-
host_entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT?.substring(0, 100),
|
|
171
|
-
host_agent: process.env.AI_AGENT?.substring(0, 100),
|
|
172
|
-
host_plugin_id: process.env.CLAUDE_PLUGIN_DATA
|
|
173
|
-
? path.basename(process.env.CLAUDE_PLUGIN_DATA).substring(0, 100) : undefined
|
|
174
|
-
});
|
|
175
151
|
// Negotiate protocol version with client
|
|
176
152
|
const requestedVersion = request.params?.protocolVersion;
|
|
177
153
|
const protocolVersion = (requestedVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion))
|
|
@@ -203,15 +179,7 @@ deferLog('info', 'Setting up request handlers...');
|
|
|
203
179
|
/**
|
|
204
180
|
* Check if a tool should be included based on current client
|
|
205
181
|
*/
|
|
206
|
-
function shouldIncludeTool(
|
|
207
|
-
// Exclude these tools for desktop-commander client (DC-specific meta-tools not useful when DC itself is the client)
|
|
208
|
-
if (currentClient?.name === 'desktop-commander-app') {
|
|
209
|
-
if (toolName === 'give_feedback_to_desktop_commander' || toolName === 'get_prompts') {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Add more conditional tool logic here as needed
|
|
214
|
-
// Example: if (toolName === 'some_tool' && currentClient?.name === 'some_client') return false;
|
|
182
|
+
function shouldIncludeTool(_toolName) {
|
|
215
183
|
return true;
|
|
216
184
|
}
|
|
217
185
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -1069,48 +1037,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1069
1037
|
readOnlyHint: true,
|
|
1070
1038
|
},
|
|
1071
1039
|
},
|
|
1072
|
-
{
|
|
1073
|
-
name: "give_feedback_to_desktop_commander",
|
|
1074
|
-
description: `
|
|
1075
|
-
Open feedback form in browser to provide feedback about MCP Device.
|
|
1076
|
-
|
|
1077
|
-
IMPORTANT: This tool simply opens the feedback form - no pre-filling available.
|
|
1078
|
-
The user will fill out the form manually in their browser.
|
|
1079
|
-
|
|
1080
|
-
WORKFLOW:
|
|
1081
|
-
1. When user agrees to give feedback, just call this tool immediately
|
|
1082
|
-
2. No need to ask questions or collect information
|
|
1083
|
-
3. Tool opens form with only usage statistics pre-filled automatically:
|
|
1084
|
-
- tool_call_count: Number of commands they've made
|
|
1085
|
-
- days_using: How many days they've used MCP Device
|
|
1086
|
-
- platform: Their operating system (Mac/Windows/Linux)
|
|
1087
|
-
- client_id: Analytics identifier
|
|
1088
|
-
|
|
1089
|
-
All survey questions will be answered directly in the form:
|
|
1090
|
-
- Job title and technical comfort level
|
|
1091
|
-
- Company URL for industry context
|
|
1092
|
-
- Other AI tools they use
|
|
1093
|
-
- MCP Device's biggest advantage
|
|
1094
|
-
- How they typically use it
|
|
1095
|
-
- Recommendation likelihood (0-10)
|
|
1096
|
-
- User study participation interest
|
|
1097
|
-
- Email and any additional feedback
|
|
1098
|
-
|
|
1099
|
-
EXAMPLE INTERACTION:
|
|
1100
|
-
User: "sure, I'll give feedback"
|
|
1101
|
-
Claude: "Perfect! Let me open the feedback form for you."
|
|
1102
|
-
[calls tool immediately]
|
|
1103
|
-
|
|
1104
|
-
No parameters are needed - just call the tool to open the form.
|
|
1105
|
-
|
|
1106
|
-
${CMD_PREFIX_DESCRIPTION}`,
|
|
1107
|
-
inputSchema: zodToJsonSchema(GiveFeedbackArgsSchema),
|
|
1108
|
-
annotations: {
|
|
1109
|
-
title: "Give Feedback",
|
|
1110
|
-
readOnlyHint: false,
|
|
1111
|
-
openWorldHint: true,
|
|
1112
|
-
},
|
|
1113
|
-
},
|
|
1114
1040
|
{
|
|
1115
1041
|
name: "get_prompts",
|
|
1116
1042
|
description: `
|
|
@@ -1190,7 +1116,7 @@ async function handleCallToolRequest(request) {
|
|
|
1190
1116
|
// add remote flag (convert to string for telemetry)
|
|
1191
1117
|
telemetryData.remote = String(metadata.remote);
|
|
1192
1118
|
// Remote calls carry the originating MCP client (e.g. openai-mcp,
|
|
1193
|
-
//
|
|
1119
|
+
// remote client) in _meta.clientInfo. Attribute this call to that remote
|
|
1194
1120
|
// client — NOT the device's own currentClient. Fall back to a sentinel
|
|
1195
1121
|
// when it's absent so the call is visibly remote-but-unattributed
|
|
1196
1122
|
// rather than masquerading as the local device client. We deliberately
|
|
@@ -1332,18 +1258,6 @@ async function handleCallToolRequest(request) {
|
|
|
1332
1258
|
};
|
|
1333
1259
|
}
|
|
1334
1260
|
break;
|
|
1335
|
-
case "give_feedback_to_desktop_commander":
|
|
1336
|
-
try {
|
|
1337
|
-
result = await giveFeedbackToDesktopCommander(args);
|
|
1338
|
-
}
|
|
1339
|
-
catch (error) {
|
|
1340
|
-
capture('server_request_error', { message: `Error in give_feedback_to_desktop_commander handler: ${error}` });
|
|
1341
|
-
result = {
|
|
1342
|
-
content: [{ type: "text", text: `Error: Failed to open feedback form` }],
|
|
1343
|
-
isError: true,
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
break;
|
|
1347
1261
|
// Terminal tools
|
|
1348
1262
|
case "start_process":
|
|
1349
1263
|
result = await handlers.handleStartProcess(args);
|
|
@@ -1422,7 +1336,7 @@ async function handleCallToolRequest(request) {
|
|
|
1422
1336
|
'get_recent_tool_calls',
|
|
1423
1337
|
'track_ui_event'
|
|
1424
1338
|
];
|
|
1425
|
-
if (!EXCLUDED_TOOLS.includes(name)) {
|
|
1339
|
+
if (!currentCallIsRemote && process.env.MCP_DEVICE_REMOTE !== 'true' && !EXCLUDED_TOOLS.includes(name)) {
|
|
1426
1340
|
toolHistory.addCall(name, args, result, duration);
|
|
1427
1341
|
}
|
|
1428
1342
|
// Track success or failure based on result
|
|
@@ -1436,77 +1350,79 @@ async function handleCallToolRequest(request) {
|
|
|
1436
1350
|
else {
|
|
1437
1351
|
await usageTracker.trackSuccess(name);
|
|
1438
1352
|
console.log(`[FEEDBACK DEBUG] Tool ${name} succeeded, checking feedback...`);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// Check if should prompt for feedback (only on successful operations)
|
|
1474
|
-
const shouldPrompt = await usageTracker.shouldPromptForFeedback();
|
|
1475
|
-
console.log(`[FEEDBACK DEBUG] Should prompt for feedback: ${shouldPrompt}`);
|
|
1476
|
-
if (shouldPrompt) {
|
|
1477
|
-
console.log(`[FEEDBACK DEBUG] Generating feedback message...`);
|
|
1478
|
-
const feedbackResult = await usageTracker.getFeedbackPromptMessage();
|
|
1479
|
-
console.log(`[FEEDBACK DEBUG] Generated variant: ${feedbackResult.variant}`);
|
|
1480
|
-
// Capture feedback prompt injection event
|
|
1481
|
-
const stats = await usageTracker.getStats();
|
|
1482
|
-
await capture('feedback_prompt_injected', {
|
|
1483
|
-
trigger_tool: name,
|
|
1484
|
-
total_calls: stats.totalToolCalls,
|
|
1485
|
-
successful_calls: stats.successfulCalls,
|
|
1486
|
-
failed_calls: stats.failedCalls,
|
|
1487
|
-
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1488
|
-
total_sessions: stats.totalSessions,
|
|
1489
|
-
message_variant: feedbackResult.variant
|
|
1490
|
-
});
|
|
1491
|
-
// Inject feedback instruction for the LLM
|
|
1492
|
-
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1493
|
-
const currentContent = result.content[0].text || '';
|
|
1494
|
-
result.content[0].text = `${currentContent}${feedbackResult.message}`;
|
|
1353
|
+
if (!currentCallIsRemote && !isRemoteClientContext(currentClient.name)) {
|
|
1354
|
+
// Check if should show onboarding (before feedback - first-time users are priority)
|
|
1355
|
+
const shouldShowOnboarding = await usageTracker.shouldShowOnboarding();
|
|
1356
|
+
console.log(`[ONBOARDING DEBUG] Should show onboarding: ${shouldShowOnboarding}`);
|
|
1357
|
+
if (shouldShowOnboarding) {
|
|
1358
|
+
console.log(`[ONBOARDING DEBUG] Generating onboarding message...`);
|
|
1359
|
+
const onboardingResult = await usageTracker.getOnboardingMessage();
|
|
1360
|
+
console.log(`[ONBOARDING DEBUG] Generated variant: ${onboardingResult.variant}`);
|
|
1361
|
+
// Capture onboarding prompt injection event
|
|
1362
|
+
const stats = await usageTracker.getStats();
|
|
1363
|
+
await capture('server_onboarding_shown', {
|
|
1364
|
+
trigger_tool: name,
|
|
1365
|
+
total_calls: stats.totalToolCalls,
|
|
1366
|
+
successful_calls: stats.successfulCalls,
|
|
1367
|
+
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1368
|
+
total_sessions: stats.totalSessions,
|
|
1369
|
+
message_variant: onboardingResult.variant
|
|
1370
|
+
});
|
|
1371
|
+
// Inject onboarding message for the LLM
|
|
1372
|
+
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1373
|
+
const currentContent = result.content[0].text || '';
|
|
1374
|
+
result.content[0].text = `${currentContent}${onboardingResult.message}`;
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
result.content = [
|
|
1378
|
+
...(result.content || []),
|
|
1379
|
+
{
|
|
1380
|
+
type: "text",
|
|
1381
|
+
text: onboardingResult.message
|
|
1382
|
+
}
|
|
1383
|
+
];
|
|
1384
|
+
}
|
|
1385
|
+
// Mark that we've shown onboarding (to prevent spam)
|
|
1386
|
+
await usageTracker.markOnboardingShown(onboardingResult.variant);
|
|
1495
1387
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1388
|
+
// Check if should prompt for feedback (only on successful operations)
|
|
1389
|
+
const shouldPrompt = await usageTracker.shouldPromptForFeedback();
|
|
1390
|
+
console.log(`[FEEDBACK DEBUG] Should prompt for feedback: ${shouldPrompt}`);
|
|
1391
|
+
if (shouldPrompt) {
|
|
1392
|
+
console.log(`[FEEDBACK DEBUG] Generating feedback message...`);
|
|
1393
|
+
const feedbackResult = await usageTracker.getFeedbackPromptMessage();
|
|
1394
|
+
console.log(`[FEEDBACK DEBUG] Generated variant: ${feedbackResult.variant}`);
|
|
1395
|
+
// Capture feedback prompt injection event
|
|
1396
|
+
const stats = await usageTracker.getStats();
|
|
1397
|
+
await capture('feedback_prompt_injected', {
|
|
1398
|
+
trigger_tool: name,
|
|
1399
|
+
total_calls: stats.totalToolCalls,
|
|
1400
|
+
successful_calls: stats.successfulCalls,
|
|
1401
|
+
failed_calls: stats.failedCalls,
|
|
1402
|
+
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1403
|
+
total_sessions: stats.totalSessions,
|
|
1404
|
+
message_variant: feedbackResult.variant
|
|
1405
|
+
});
|
|
1406
|
+
// Inject feedback instruction for the LLM
|
|
1407
|
+
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1408
|
+
const currentContent = result.content[0].text || '';
|
|
1409
|
+
result.content[0].text = `${currentContent}${feedbackResult.message}`;
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
result.content = [
|
|
1413
|
+
...(result.content || []),
|
|
1414
|
+
{
|
|
1415
|
+
type: "text",
|
|
1416
|
+
text: feedbackResult.message
|
|
1417
|
+
}
|
|
1418
|
+
];
|
|
1419
|
+
}
|
|
1420
|
+
// Mark that we've prompted (to prevent spam)
|
|
1421
|
+
await usageTracker.markFeedbackPrompted();
|
|
1504
1422
|
}
|
|
1505
|
-
//
|
|
1506
|
-
await
|
|
1423
|
+
// Check if should prompt about Docker environment
|
|
1424
|
+
result = await processDockerPrompt(result, name);
|
|
1507
1425
|
}
|
|
1508
|
-
// Check if should prompt about Docker environment
|
|
1509
|
-
result = await processDockerPrompt(result, name);
|
|
1510
1426
|
}
|
|
1511
1427
|
// If the caller sent parameters this tool does not support, Zod silently
|
|
1512
1428
|
// strips them. Prepend a corrective warning so the model knows they were
|
package/dist/terminal-manager.js
CHANGED
|
@@ -8,7 +8,7 @@ import { analyzeProcessState } from './utils/process-detection.js';
|
|
|
8
8
|
* Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before
|
|
9
9
|
* spawning child shells.
|
|
10
10
|
*
|
|
11
|
-
* On some Windows
|
|
11
|
+
* On some Windows host launches the server process inherits a
|
|
12
12
|
* broken PATHEXT (observed as ".CPL" only). Because we build the child env from
|
|
13
13
|
* { ...process.env }, that broken value would propagate into every spawned
|
|
14
14
|
* shell, stripping ".EXE" and breaking resolution of git / node / python / rg /
|
|
@@ -205,6 +205,21 @@ export class TerminalManager {
|
|
|
205
205
|
// Spawn the process with appropriate arguments
|
|
206
206
|
const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
|
|
207
207
|
let output = '';
|
|
208
|
+
// spawn() reports resolution/launch failures asynchronously via 'error'.
|
|
209
|
+
// A listener-less ChildProcess error becomes an uncaughtException and kills
|
|
210
|
+
// the inherited MCP server. Attach before any early-return path and keep
|
|
211
|
+
// the listener for the process lifetime.
|
|
212
|
+
let forwardProcessError = null;
|
|
213
|
+
let pendingProcessError = null;
|
|
214
|
+
childProcess.on('error', (err) => {
|
|
215
|
+
if (forwardProcessError) {
|
|
216
|
+
forwardProcessError(err);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
pendingProcessError = err;
|
|
220
|
+
console.error(`Process error for "${command}": ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
208
223
|
// Ensure childProcess.pid is defined before proceeding
|
|
209
224
|
if (!childProcess.pid) {
|
|
210
225
|
// Return a consistent error object instead of throwing
|
|
@@ -259,6 +274,31 @@ export class TerminalManager {
|
|
|
259
274
|
}
|
|
260
275
|
resolve(result);
|
|
261
276
|
};
|
|
277
|
+
// Once resolveOnce exists, route runtime/spawn errors into the normal
|
|
278
|
+
// command result path so callers do not hang and the session is retired.
|
|
279
|
+
forwardProcessError = (err) => {
|
|
280
|
+
// A late ChildProcess 'error' can arrive after the caller has already
|
|
281
|
+
// received a live/background session. Do not silently retire that
|
|
282
|
+
// session; the normal 'exit' event remains authoritative for cleanup.
|
|
283
|
+
if (resolved) {
|
|
284
|
+
this.appendToLineBuffer(session, `\nProcess error: ${err.message}\n`);
|
|
285
|
+
console.error(`Process error for "${command}": ${err.message}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (childProcess.pid)
|
|
289
|
+
this.sessions.delete(childProcess.pid);
|
|
290
|
+
exitReason = 'process_exit';
|
|
291
|
+
resolveOnce({
|
|
292
|
+
pid: childProcess.pid ?? -1,
|
|
293
|
+
output: output + `\nProcess error: ${err.message}`,
|
|
294
|
+
isBlocked: false
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
// Spawn failures normally arrive on the next tick, but replay any error
|
|
298
|
+
// emitted between spawn() and resolver initialization.
|
|
299
|
+
if (pendingProcessError) {
|
|
300
|
+
forwardProcessError(pendingProcessError);
|
|
301
|
+
}
|
|
262
302
|
childProcess.stdout.on('data', (data) => {
|
|
263
303
|
const text = data.toString();
|
|
264
304
|
const now = Date.now();
|
package/dist/tools/config.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export declare function getConfig(): Promise<{
|
|
|
65
65
|
availableShells: string[];
|
|
66
66
|
};
|
|
67
67
|
entries: {
|
|
68
|
-
key: "
|
|
68
|
+
key: "blockedCommands" | "defaultShell" | "allowedDirectories" | "telemetryEnabled" | "fileWriteLineLimit" | "fileReadLineLimit";
|
|
69
69
|
value: unknown;
|
|
70
70
|
valueType: "string" | "number" | "boolean" | "array";
|
|
71
71
|
editable: boolean;
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -84,10 +84,10 @@ function buildPermissionError(filePath, errCode) {
|
|
|
84
84
|
` 5. The app does not have permission to access this location (macOS Full Disk Access).`,
|
|
85
85
|
];
|
|
86
86
|
if (isMac) {
|
|
87
|
-
lines.push(` → Go to System Settings → Privacy & Security → Full Disk Access and enable
|
|
87
|
+
lines.push(` → Go to System Settings → Privacy & Security → Full Disk Access and enable the terminal or runtime hosting MCP Device.`);
|
|
88
88
|
lines.push(` → To open that pane directly, run in terminal:`);
|
|
89
89
|
lines.push(` open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"`);
|
|
90
|
-
lines.push(` Then
|
|
90
|
+
lines.push(` Then enable the toggle for the application that launches MCP Device.`);
|
|
91
91
|
}
|
|
92
92
|
else {
|
|
93
93
|
lines.push(` → Check that the app has permission to access this file location.`);
|
|
@@ -265,7 +265,7 @@ export async function validatePath(requestedPath) {
|
|
|
265
265
|
const pathForNextCheck = resolvedRealPath ?? absoluteOriginal;
|
|
266
266
|
// Direct Gateway device workers are already authenticated and policy-gated by
|
|
267
267
|
// the central gateway. In that dedicated remote context, do not inherit a
|
|
268
|
-
// workstation's historical
|
|
268
|
+
// workstation's historical directory allowlist. An optional
|
|
269
269
|
// MCP_GATEWAY_ALLOWED_ROOTS restriction is enforced by GatewayToolAdapter before
|
|
270
270
|
// calls reach this filesystem layer.
|
|
271
271
|
const remoteGatewayContext = process.env.MCP_DEVICE_REMOTE === 'true';
|
|
@@ -9,7 +9,7 @@ const isUrl = (source) => source.startsWith('http://') || source.startsWith('htt
|
|
|
9
9
|
let cachedChromePath = null; // null = not checked yet
|
|
10
10
|
let chromeCheckPromise = null;
|
|
11
11
|
/**
|
|
12
|
-
* Get
|
|
12
|
+
* Get the local engine's private Puppeteer cache directory.
|
|
13
13
|
*/
|
|
14
14
|
function getPuppeteerCacheDir() {
|
|
15
15
|
return join(dirname(CONFIG_FILE), 'puppeteer-cache');
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -117,15 +117,15 @@ export declare const WriteFileArgsSchema: z.ZodObject<{
|
|
|
117
117
|
mode: z.ZodDefault<z.ZodEnum<["rewrite", "append"]>>;
|
|
118
118
|
origin: z.ZodOptional<z.ZodEnum<["ui", "llm"]>>;
|
|
119
119
|
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
mode: "rewrite" | "append";
|
|
120
121
|
path: string;
|
|
121
122
|
content: string;
|
|
122
|
-
mode: "rewrite" | "append";
|
|
123
123
|
origin?: "ui" | "llm" | undefined;
|
|
124
124
|
}, {
|
|
125
125
|
path: string;
|
|
126
126
|
content: string;
|
|
127
|
-
origin?: "ui" | "llm" | undefined;
|
|
128
127
|
mode?: "rewrite" | "append" | undefined;
|
|
128
|
+
origin?: "ui" | "llm" | undefined;
|
|
129
129
|
}>;
|
|
130
130
|
export declare const PdfInsertOperationSchema: z.ZodObject<{
|
|
131
131
|
type: z.ZodLiteral<"insert">;
|
|
@@ -348,7 +348,6 @@ export declare const InteractWithProcessArgsSchema: z.ZodObject<{
|
|
|
348
348
|
wait_for_prompt?: boolean | undefined;
|
|
349
349
|
}>;
|
|
350
350
|
export declare const GetUsageStatsArgsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
351
|
-
export declare const GiveFeedbackArgsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
352
351
|
export declare const StartSearchArgsSchema: z.ZodObject<{
|
|
353
352
|
path: z.ZodString;
|
|
354
353
|
pattern: z.ZodString;
|
package/dist/tools/schemas.js
CHANGED
|
@@ -153,15 +153,6 @@ export const InteractWithProcessArgsSchema = z.object({
|
|
|
153
153
|
});
|
|
154
154
|
// Usage stats schema
|
|
155
155
|
export const GetUsageStatsArgsSchema = z.object({});
|
|
156
|
-
// Feedback tool schema - no pre-filled parameters, all user input
|
|
157
|
-
export const GiveFeedbackArgsSchema = z.object({
|
|
158
|
-
// No parameters needed - form will be filled manually by user
|
|
159
|
-
// Only auto-filled hidden fields remain:
|
|
160
|
-
// - tool_call_count (auto)
|
|
161
|
-
// - days_using (auto)
|
|
162
|
-
// - platform (auto)
|
|
163
|
-
// - client_id (auto)
|
|
164
|
-
});
|
|
165
156
|
// Search schemas (renamed for natural language)
|
|
166
157
|
export const StartSearchArgsSchema = z.object({
|
|
167
158
|
path: z.string(),
|
|
@@ -236,7 +227,6 @@ export const toolArgSchemas = {
|
|
|
236
227
|
kill_process: KillProcessArgsSchema,
|
|
237
228
|
get_usage_stats: GetUsageStatsArgsSchema,
|
|
238
229
|
get_recent_tool_calls: GetRecentToolCallsArgsSchema,
|
|
239
|
-
give_feedback_to_desktop_commander: GiveFeedbackArgsSchema,
|
|
240
230
|
get_prompts: GetPromptsArgsSchema,
|
|
241
231
|
track_ui_event: TrackUiEventArgsSchema,
|
|
242
232
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<head>
|
|
7
7
|
<meta charset="utf-8" />
|
|
8
8
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9
|
-
<title>
|
|
9
|
+
<title>MCP Device File Preview</title>
|
|
10
10
|
<link rel="stylesheet" href="./styles.css" />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -141,7 +141,7 @@ body.dc-ready {
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
/*
|
|
144
|
-
* App-specific styling for File Preview — tuned
|
|
144
|
+
* App-specific styling for File Preview — tuned for the MCP Device UI.
|
|
145
145
|
*/
|
|
146
146
|
:root {
|
|
147
147
|
--code-bg: var(--panel);
|
|
@@ -166,7 +166,7 @@ body.dc-ready {
|
|
|
166
166
|
--toc-rail-width: 320px;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/* ── Panel (
|
|
169
|
+
/* ── Panel (compact card) ── */
|
|
170
170
|
|
|
171
171
|
.panel {
|
|
172
172
|
margin-top: 6px;
|
package/dist/utils/capture.d.ts
CHANGED
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
export declare function runInUiOriginCallContext<T>(fn: () => T): T;
|
|
2
2
|
export declare function isInsideUiOriginCall(): boolean;
|
|
3
|
-
/**
|
|
4
|
-
* Hard kill-switch for telemetry via environment variable.
|
|
5
|
-
*
|
|
6
|
-
* Independent of the persisted `telemetryEnabled` config so that tests, CI and
|
|
7
|
-
* one-off runs can suppress all analytics without mutating the user's config.
|
|
8
|
-
* Set DESKTOP_COMMANDER_DISABLE_TELEMETRY to 1/true/yes/on to disable.
|
|
9
|
-
*/
|
|
10
3
|
export declare function isTelemetryDisabledByEnv(): boolean;
|
|
11
|
-
/**
|
|
12
|
-
* Sanitizes error objects to remove potentially sensitive information like file paths
|
|
13
|
-
* @param error Error object or string to sanitize
|
|
14
|
-
* @returns An object with sanitized message and optional error code
|
|
15
|
-
*/
|
|
16
4
|
export declare function sanitizeError(error: any): {
|
|
17
5
|
message: string;
|
|
18
6
|
code?: string;
|
|
19
7
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export declare const captureBase: (captureURL: string, event: string, properties?: any) => Promise<void>;
|
|
26
|
-
export declare const capture: (event: string, properties?: any) => Promise<void>;
|
|
27
|
-
export declare const capture_call_tool: (event: string, properties?: any) => Promise<void>;
|
|
28
|
-
export declare const capture_ui_event: (event: string, properties?: any) => Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* Wrapper for capture() that automatically adds remote flag for device telemetry
|
|
31
|
-
* Also adds additional privacy filtering to remove sensitive identity information
|
|
32
|
-
* @param event Event name
|
|
33
|
-
* @param properties Optional event properties
|
|
34
|
-
*/
|
|
35
|
-
export declare const captureRemote: (event: string, properties?: any) => Promise<void>;
|
|
8
|
+
export declare const captureBase: (_captureURL: string, _event: string, _properties?: any) => Promise<void>;
|
|
9
|
+
export declare const capture: (_event: string, _properties?: any) => Promise<void>;
|
|
10
|
+
export declare const capture_call_tool: (_event: string, _properties?: any) => Promise<void>;
|
|
11
|
+
export declare const capture_ui_event: (_event: string, _properties?: any) => Promise<void>;
|
|
12
|
+
export declare const captureRemote: (_event: string, _properties?: any) => Promise<void>;
|