@minicor/mcp-server 4.19.2 → 4.19.3
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 +5 -2
- package/dist/__tests__/agent-service-client.test.d.ts +2 -0
- package/dist/__tests__/agent-service-client.test.d.ts.map +1 -0
- package/dist/__tests__/agent-service-client.test.js +24 -0
- package/dist/__tests__/agent-service-client.test.js.map +1 -0
- package/dist/__tests__/dev-mode-draft.test.js +3 -1
- package/dist/__tests__/dev-mode-draft.test.js.map +1 -1
- package/dist/__tests__/laminar-client.test.js +5 -3
- package/dist/__tests__/laminar-client.test.js.map +1 -1
- package/dist/__tests__/vm-broker.test.js +35 -8
- package/dist/__tests__/vm-broker.test.js.map +1 -1
- package/dist/agent-service-client.d.ts +4 -0
- package/dist/agent-service-client.d.ts.map +1 -1
- package/dist/agent-service-client.js.map +1 -1
- package/dist/laminar-client.d.ts +1 -0
- package/dist/laminar-client.d.ts.map +1 -1
- package/dist/laminar-client.js.map +1 -1
- package/dist/server-surface.d.ts.map +1 -1
- package/dist/server-surface.js +5 -2
- package/dist/server-surface.js.map +1 -1
- package/dist/tools/agents.d.ts.map +1 -1
- package/dist/tools/agents.js +7 -2
- package/dist/tools/agents.js.map +1 -1
- package/dist/tools/desktop-settlement.d.ts +5 -0
- package/dist/tools/desktop-settlement.d.ts.map +1 -0
- package/dist/tools/desktop-settlement.js +28 -0
- package/dist/tools/desktop-settlement.js.map +1 -0
- package/dist/tools/dev-mode-draft.d.ts.map +1 -1
- package/dist/tools/dev-mode-draft.js +11 -2
- package/dist/tools/dev-mode-draft.js.map +1 -1
- package/dist/tools/vm-desktop.d.ts +3 -0
- package/dist/tools/vm-desktop.d.ts.map +1 -0
- package/dist/tools/vm-desktop.js +532 -0
- package/dist/tools/vm-desktop.js.map +1 -0
- package/dist/tools/vm-rpa.d.ts.map +1 -1
- package/dist/tools/vm-rpa.js +33 -17
- package/dist/tools/vm-rpa.js.map +1 -1
- package/dist/tools/vm.d.ts.map +1 -1
- package/dist/tools/vm.js +8 -498
- package/dist/tools/vm.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/vm.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VM provisioning, MDS configuration and installation tools.
|
|
3
|
+
* Uses the VM service and Azure management APIs; direct desktop access is registered separately.
|
|
4
|
+
*/
|
|
1
5
|
import { z } from "zod";
|
|
2
|
-
import { ok, text,
|
|
6
|
+
import { ok, text, DIRECT_VM_TOOLS_DISABLED_HOSTED } from "../helpers.js";
|
|
3
7
|
import * as lds from "../lds-client.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { fetchVmCredentials, fetchVmCredentialsSoft } from "../vm-target.js";
|
|
9
|
+
import { register as registerVmDesktop } from "./vm-desktop.js";
|
|
6
10
|
import { registerVmDiscovery } from "./discovery.js";
|
|
7
11
|
const regionParam = z
|
|
8
12
|
.enum(["us", "ca"])
|
|
9
13
|
.optional()
|
|
10
14
|
.describe("Region (us or ca). Auto-detected from workspace if omitted.");
|
|
11
|
-
const targetUserParam = z
|
|
12
|
-
.string()
|
|
13
|
-
.optional()
|
|
14
|
-
.describe("Windows account whose session runs this (broker/multi-session VMs only — REQUIRED there, MDS never picks a user). Omit on standalone VMs. vm_list_sessions shows the registered users.");
|
|
15
15
|
/** A robot-user catalog entry as accepted by the VM service (MDSRegisteredUserInput). */
|
|
16
16
|
const mdsRegisteredUserSchema = z.object({
|
|
17
17
|
username: z.string().describe("Local Windows account name"),
|
|
@@ -36,21 +36,9 @@ export function register(deps, directVmTools = true) {
|
|
|
36
36
|
return text(DIRECT_VM_TOOLS_DISABLED_HOSTED);
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
|
-
async function requireTarget(args) {
|
|
40
|
-
const disabled = requireDirect();
|
|
41
|
-
if (disabled)
|
|
42
|
-
return { ok: false, res: disabled };
|
|
43
|
-
const resolved = await resolveVmTarget(deps, args);
|
|
44
|
-
if (!resolved.ok)
|
|
45
|
-
return { ok: false, res: text(resolved.message) };
|
|
46
|
-
return { ok: true, target: resolved.target };
|
|
47
|
-
}
|
|
48
|
-
function refreshStoredMode(target, mode) {
|
|
49
|
-
if (state.ldsConnection?.url === target.url)
|
|
50
|
-
state.ldsConnection.mode = mode;
|
|
51
|
-
}
|
|
52
39
|
// ── VM discovery ─────────────────────────────────────────────
|
|
53
40
|
registerVmDiscovery(deps);
|
|
41
|
+
registerVmDesktop(deps, directVmTools);
|
|
54
42
|
// ── VM deployment ──────────────────────────────────────────
|
|
55
43
|
server.tool("deploy_vm", "Provision a new Windows VM on Azure. Uses smart region selection — pass 'us' or 'canada' " +
|
|
56
44
|
"and the service finds a region with available quota. Deployment is async (~5-10 min). " +
|
|
@@ -126,381 +114,6 @@ export function register(deps, directVmTools = true) {
|
|
|
126
114
|
return text(`VM deployment failed: ${e.message}`);
|
|
127
115
|
}
|
|
128
116
|
});
|
|
129
|
-
// ── Connection management ─────────────────────────────────
|
|
130
|
-
server.tool("vm_connect", "Connect to a VM. Provide vmId (from vm_list) to auto-fill credentials, or provide url directly. Discovers the MDS mode from /health: on a multi-session (broker) VM, execute/screenshot calls additionally need targetUser. The connection is remembered only by servers that outlive one call (stdio/embedded) — on the hosted server, pass vmId + workspaceId to each vm_* tool instead.", {
|
|
131
|
-
vmId: z.number().optional().describe("VM ID from vm_list — auto-fills url, apiKey, serviceId"),
|
|
132
|
-
workspaceId: z
|
|
133
|
-
.number()
|
|
134
|
-
.optional()
|
|
135
|
-
.describe("Workspace that owns the VM. With vmId, resolves the VM straight from the VM service — no prior vm_list needed."),
|
|
136
|
-
url: z.string().optional().describe("MDS base URL (Cloudflare Tunnel) — use this OR vmId"),
|
|
137
|
-
apiKey: z.string().optional().describe("MDS API key (not needed with vmId)"),
|
|
138
|
-
serviceId: z.string().optional().describe("MDS service ID (not needed with vmId)"),
|
|
139
|
-
region: regionParam,
|
|
140
|
-
}, async ({ vmId, workspaceId, url, apiKey, serviceId, region }) => {
|
|
141
|
-
const disabled = requireDirect();
|
|
142
|
-
if (disabled)
|
|
143
|
-
return disabled;
|
|
144
|
-
let connectUrl = url;
|
|
145
|
-
let connectApiKey = apiKey;
|
|
146
|
-
let connectServiceId = serviceId;
|
|
147
|
-
let vmName;
|
|
148
|
-
if (vmId) {
|
|
149
|
-
const vm = state.getCachedVm(vmId);
|
|
150
|
-
if (vm) {
|
|
151
|
-
if (!vm.tunnel_url) {
|
|
152
|
-
return text(`VM "${vm.vm_name}" has no tunnel URL — it may not be running.`);
|
|
153
|
-
}
|
|
154
|
-
connectUrl = vm.tunnel_url;
|
|
155
|
-
connectApiKey = vm.api_key ?? undefined;
|
|
156
|
-
connectServiceId = vm.service_id ?? undefined;
|
|
157
|
-
vmName = vm.vm_name;
|
|
158
|
-
// Redacted list (newer VM service): resolve the MDS credentials via
|
|
159
|
-
// the per-VM credentials endpoint instead of failing auth later.
|
|
160
|
-
if (!connectApiKey && vm.workspace_id) {
|
|
161
|
-
const wsId = Number(vm.workspace_id);
|
|
162
|
-
const creds = await fetchVmCredentialsSoft(deps, vm.workspace_id, vmId, region ??
|
|
163
|
-
(Number.isNaN(wsId) ? undefined : state.getWorkspaceRegion(wsId)));
|
|
164
|
-
connectApiKey = creds.apiKey;
|
|
165
|
-
connectServiceId = connectServiceId ?? creds.serviceId;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else if (workspaceId !== undefined) {
|
|
169
|
-
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
170
|
-
const resolved = await fetchVmCredentials(deps, workspaceId, vmId, wsRegion);
|
|
171
|
-
if (!resolved.ok)
|
|
172
|
-
return text(resolved.message);
|
|
173
|
-
if (!resolved.creds.tunnelUrl) {
|
|
174
|
-
return text(`VM ${vmId} has no tunnel URL — MDS may not be installed or running. Check vm_list, or get_install_status if an install is in flight.`);
|
|
175
|
-
}
|
|
176
|
-
connectUrl = resolved.creds.tunnelUrl;
|
|
177
|
-
connectApiKey = resolved.creds.apiKey;
|
|
178
|
-
connectServiceId = resolved.creds.serviceId;
|
|
179
|
-
vmName = resolved.creds.vmName;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
return text("VM not found in cache. Run vm_list first to discover available VMs, or pass workspaceId alongside vmId to resolve it directly.");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!connectUrl) {
|
|
186
|
-
return text("Provide either vmId (from vm_list) or url to connect.");
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
const health = await lds.health(connectUrl);
|
|
190
|
-
const mode = lds.modeFromHealth(health);
|
|
191
|
-
state.setLdsConnection({
|
|
192
|
-
url: connectUrl,
|
|
193
|
-
apiKey: connectApiKey,
|
|
194
|
-
serviceId: connectServiceId,
|
|
195
|
-
vmId,
|
|
196
|
-
vmName,
|
|
197
|
-
mode,
|
|
198
|
-
});
|
|
199
|
-
const brokerMode = mode === "broker";
|
|
200
|
-
let registeredUsers;
|
|
201
|
-
if (brokerMode) {
|
|
202
|
-
try {
|
|
203
|
-
const status = await lds.status(connectUrl, state.ldsAuth());
|
|
204
|
-
registeredUsers = status.users.map((u) => u.user);
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
/* surface users lazily via vm_list_sessions instead */
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return ok({
|
|
211
|
-
connected: true,
|
|
212
|
-
url: connectUrl,
|
|
213
|
-
vmId,
|
|
214
|
-
vmName,
|
|
215
|
-
mode,
|
|
216
|
-
authenticated: !!(connectApiKey && connectServiceId),
|
|
217
|
-
service: health,
|
|
218
|
-
...(registeredUsers ? { registeredUsers } : {}),
|
|
219
|
-
next_steps: [
|
|
220
|
-
...(brokerMode
|
|
221
|
-
? [
|
|
222
|
-
"This VM is a multi-session broker: EVERY vm_execute_script / vm_screenshot / debug_rpa_step call must include targetUser (MDS never picks a user itself)",
|
|
223
|
-
"Use vm_list_sessions to see registered users and their session/agent state, then ask the user which account to target",
|
|
224
|
-
"Use vm_start_session / vm_stop_session to manage user sessions",
|
|
225
|
-
]
|
|
226
|
-
: [
|
|
227
|
-
"Take vm_screenshot to see the current desktop state",
|
|
228
|
-
"Use vm_inspect_ui mode 'window_list' to see running apps",
|
|
229
|
-
"Use the build-rpa-workflow prompt for guided automation building",
|
|
230
|
-
"Run Python scripts via vm_execute_script (CDP for browsers, uiautomation for desktop)",
|
|
231
|
-
]),
|
|
232
|
-
...(vmId !== undefined
|
|
233
|
-
? [
|
|
234
|
-
`Pass vmId ${vmId}${workspaceId !== undefined ? ` + workspaceId ${workspaceId}` : " + workspaceId"} on each vm_* call to target this VM without depending on the connection persisting (required on the hosted server)`,
|
|
235
|
-
]
|
|
236
|
-
: []),
|
|
237
|
-
],
|
|
238
|
-
automation_approaches: {
|
|
239
|
-
browser: "CDP via Python websocket to Chrome on port 9222 → vm_execute_script",
|
|
240
|
-
desktop: "uiautomation/pywinauto/pyautogui → vm_execute_script",
|
|
241
|
-
api: "Python requests/httpx when APIs are available → vm_execute_script",
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch (e) {
|
|
246
|
-
return text(`Failed to connect to VM at ${connectUrl}: ${e.message}`);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
server.tool("vm_disconnect", "Disconnect from the current VM", {}, async () => {
|
|
250
|
-
const disabled = requireDirect();
|
|
251
|
-
if (disabled)
|
|
252
|
-
return disabled;
|
|
253
|
-
state.setLdsConnection(null);
|
|
254
|
-
return text("Disconnected from VM.");
|
|
255
|
-
});
|
|
256
|
-
server.tool("vm_status", "Check health/status of a VM, including its MDS mode (standalone or broker)", {
|
|
257
|
-
...vmTargetParams,
|
|
258
|
-
region: regionParam,
|
|
259
|
-
}, async ({ vmId, workspaceId, region }) => {
|
|
260
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
261
|
-
if (!t.ok)
|
|
262
|
-
return t.res;
|
|
263
|
-
const { target } = t;
|
|
264
|
-
try {
|
|
265
|
-
const health = await lds.health(target.url);
|
|
266
|
-
const mode = lds.modeFromHealth(health);
|
|
267
|
-
// Keep the stored mode fresh across MDS restarts.
|
|
268
|
-
refreshStoredMode(target, mode);
|
|
269
|
-
return ok({
|
|
270
|
-
connected: true,
|
|
271
|
-
url: target.url,
|
|
272
|
-
vmId: target.vmId,
|
|
273
|
-
vmName: target.vmName,
|
|
274
|
-
mode,
|
|
275
|
-
...health,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
catch (e) {
|
|
279
|
-
return text(`VM unreachable: ${e.message}`);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
// ── Screenshot ────────────────────────────────────────────
|
|
283
|
-
server.tool("vm_screenshot", "Take a full-screen screenshot of the VM. Use for verifying actions worked and seeing the current desktop/browser state. For reading small text, prefer vm_screenshot_region. On a broker VM, pass targetUser to capture that user's session desktop.", {
|
|
284
|
-
targetUser: targetUserParam,
|
|
285
|
-
...vmTargetParams,
|
|
286
|
-
region: regionParam,
|
|
287
|
-
}, async ({ targetUser, vmId, workspaceId, region }) => {
|
|
288
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
289
|
-
if (!t.ok)
|
|
290
|
-
return t.res;
|
|
291
|
-
const { target } = t;
|
|
292
|
-
try {
|
|
293
|
-
const res = await lds.screenshot(target.url, target.auth, targetUser);
|
|
294
|
-
const { width, height, size_bytes, capture_duration_ms, timestamp } = res.metadata;
|
|
295
|
-
return {
|
|
296
|
-
content: [
|
|
297
|
-
{
|
|
298
|
-
type: "image",
|
|
299
|
-
data: res.image,
|
|
300
|
-
mimeType: "image/png",
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
type: "text",
|
|
304
|
-
text: json({
|
|
305
|
-
width,
|
|
306
|
-
height,
|
|
307
|
-
size_bytes,
|
|
308
|
-
capture_duration_ms,
|
|
309
|
-
timestamp,
|
|
310
|
-
}),
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
catch (e) {
|
|
316
|
-
return text(`Screenshot failed: ${e.message}`);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
server.tool("vm_screenshot_region", "Zoom into a specific screen region. Use for reading small text or verifying specific UI elements with better resolution than full screenshots.", {
|
|
320
|
-
x: z.number().describe("Left X coordinate"),
|
|
321
|
-
y: z.number().describe("Top Y coordinate"),
|
|
322
|
-
width: z.number().describe("Region width in pixels"),
|
|
323
|
-
height: z.number().describe("Region height in pixels"),
|
|
324
|
-
targetUser: targetUserParam,
|
|
325
|
-
...vmTargetParams,
|
|
326
|
-
region: z
|
|
327
|
-
.enum(["us", "ca"])
|
|
328
|
-
.optional()
|
|
329
|
-
.describe("Minicor region (us or ca) the workspace lives in — NOT the screen region. Auto-detected from workspace if omitted."),
|
|
330
|
-
}, async ({ x, y, width, height, targetUser, vmId, workspaceId, region }) => {
|
|
331
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
332
|
-
if (!t.ok)
|
|
333
|
-
return t.res;
|
|
334
|
-
const { target } = t;
|
|
335
|
-
const x2 = x + width;
|
|
336
|
-
const y2 = y + height;
|
|
337
|
-
const script = `
|
|
338
|
-
import json, base64, io
|
|
339
|
-
from PIL import ImageGrab
|
|
340
|
-
|
|
341
|
-
img = ImageGrab.grab(bbox=(${x}, ${y}, ${x2}, ${y2}))
|
|
342
|
-
buf = io.BytesIO()
|
|
343
|
-
img.save(buf, format="PNG")
|
|
344
|
-
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
345
|
-
print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
346
|
-
`.trim();
|
|
347
|
-
try {
|
|
348
|
-
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
349
|
-
if (!res.success) {
|
|
350
|
-
return text(`Region screenshot failed: ${res.stderr || "unknown error"}`);
|
|
351
|
-
}
|
|
352
|
-
const parsed = JSON.parse(res.stdout);
|
|
353
|
-
return {
|
|
354
|
-
content: [
|
|
355
|
-
{
|
|
356
|
-
type: "image",
|
|
357
|
-
data: parsed.image,
|
|
358
|
-
mimeType: "image/png",
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
type: "text",
|
|
362
|
-
text: json({
|
|
363
|
-
width: parsed.width,
|
|
364
|
-
height: parsed.height,
|
|
365
|
-
region: { x, y, width, height },
|
|
366
|
-
}),
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
catch (e) {
|
|
372
|
-
return text(`Region screenshot failed: ${e.message}`);
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
// ── Script execution ──────────────────────────────────────
|
|
376
|
-
server.tool("vm_execute_script", "Execute a Python script on the VM. This is the primary tool for ALL automation: CDP websocket for browser control, uiautomation/pyautogui for desktop apps, requests/httpx for API calls. Returns stdout, stderr, exit code, and timing. On a broker VM, targetUser is required — MDS routes the script to that user's session.", {
|
|
377
|
-
script: z.string().describe("Python script to execute"),
|
|
378
|
-
executionId: z.string().optional().describe("Execution ID for tracking"),
|
|
379
|
-
flowId: z.string().optional().describe("Flow ID for tracking"),
|
|
380
|
-
targetUser: targetUserParam,
|
|
381
|
-
...vmTargetParams,
|
|
382
|
-
region: regionParam,
|
|
383
|
-
}, async ({ script, executionId, flowId, targetUser, vmId, workspaceId, region }) => {
|
|
384
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
385
|
-
if (!t.ok)
|
|
386
|
-
return t.res;
|
|
387
|
-
const { target } = t;
|
|
388
|
-
try {
|
|
389
|
-
const res = await lds.execute(target.url, script, { executionId, flowId, targetUser }, target.auth);
|
|
390
|
-
return ok({
|
|
391
|
-
success: res.success,
|
|
392
|
-
exitCode: res.exitCode,
|
|
393
|
-
stdout: res.stdout,
|
|
394
|
-
stderr: res.stderr,
|
|
395
|
-
executionTimeMs: res.executionTimeMs,
|
|
396
|
-
skipped: res.skipped,
|
|
397
|
-
stopped: res.stopped,
|
|
398
|
-
resultData: res.resultData,
|
|
399
|
-
// Broker attribution (absent on standalone).
|
|
400
|
-
...(res.targetUser !== undefined ? { targetUser: res.targetUser } : {}),
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
catch (e) {
|
|
404
|
-
return text(`Execution failed: ${e.message}`);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
// ── Execution status ──────────────────────────────────────
|
|
408
|
-
// vm_execution_control is gone (MDS removed /execute/control); do not re-add.
|
|
409
|
-
server.tool("vm_execution_status", "Get current execution status on the VM (active script, progress, queue). On a broker VM the plain call returns the router-wide status for every user; pass targetUser (and optionally executionId) to read one user's live execution progress instead.", {
|
|
410
|
-
targetUser: targetUserParam,
|
|
411
|
-
executionId: z
|
|
412
|
-
.string()
|
|
413
|
-
.optional()
|
|
414
|
-
.describe("Look up progress for a specific executionId (with or without targetUser)"),
|
|
415
|
-
...vmTargetParams,
|
|
416
|
-
region: regionParam,
|
|
417
|
-
}, async ({ targetUser, executionId, vmId, workspaceId, region }) => {
|
|
418
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
419
|
-
if (!t.ok)
|
|
420
|
-
return t.res;
|
|
421
|
-
const { target } = t;
|
|
422
|
-
try {
|
|
423
|
-
// A targeted lookup routes to /execute/progress, which brokers
|
|
424
|
-
// forward to that user's agent; the untargeted call stays on
|
|
425
|
-
// /execute/status (router-wide on brokers, slots on standalone).
|
|
426
|
-
if (targetUser || executionId) {
|
|
427
|
-
const progress = await lds.executionProgress(target.url, target.auth, {
|
|
428
|
-
targetUser,
|
|
429
|
-
executionId,
|
|
430
|
-
});
|
|
431
|
-
return ok(progress);
|
|
432
|
-
}
|
|
433
|
-
const status = await lds.executionStatus(target.url, target.auth);
|
|
434
|
-
return ok(status);
|
|
435
|
-
}
|
|
436
|
-
catch (e) {
|
|
437
|
-
return text(`Failed to get execution status: ${e.message}`);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
// ── User sessions (multi-session / broker VMs) ─────────────
|
|
441
|
-
server.tool("vm_list_sessions", "List the registered users on a multi-session (broker) VM with their session and agent state (GET /status). Use this to pick a targetUser for vm_execute_script / vm_screenshot, or to show the user which accounts are available. Requires API-key auth (target by vmId + workspaceId, or connect with vmId or apiKey+serviceId).", {
|
|
442
|
-
...vmTargetParams,
|
|
443
|
-
region: regionParam,
|
|
444
|
-
}, async ({ vmId, workspaceId, region }) => {
|
|
445
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
446
|
-
if (!t.ok)
|
|
447
|
-
return t.res;
|
|
448
|
-
const { target } = t;
|
|
449
|
-
try {
|
|
450
|
-
// /status only exists on brokers; probe mode so a standalone 404 reads clearly.
|
|
451
|
-
const mode = await lds.discoverMode(target.url);
|
|
452
|
-
refreshStoredMode(target, mode);
|
|
453
|
-
if (mode !== "broker") {
|
|
454
|
-
return text(`This VM runs MDS in '${mode}' mode — per-user sessions only exist on multi-session (broker) VMs. Standalone tools work without targetUser.`);
|
|
455
|
-
}
|
|
456
|
-
const status = await lds.status(target.url, target.auth);
|
|
457
|
-
return ok({
|
|
458
|
-
mode,
|
|
459
|
-
broker: status.broker,
|
|
460
|
-
users: status.users,
|
|
461
|
-
queues: status.queues,
|
|
462
|
-
hint: "Pass one of these users as targetUser to vm_execute_script / vm_screenshot / debug_rpa_step. State 'active' means a live agent; 'stopped'/'provisioned' users need vm_start_session (or any job) first.",
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
catch (e) {
|
|
466
|
-
return text(`Failed to list sessions: ${e.message}`);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
server.tool("vm_start_session", "Start (or reconnect) a registered user's session on the connected broker VM (POST /sessions/{user}/start). Blocks until the session is up and its agent is registered. Fails with a typed error when the session keeper is disabled (session_start_disabled) or the user is not registered.", {
|
|
470
|
-
user: z.string().describe("Registered Windows account name (see vm_list_sessions)"),
|
|
471
|
-
...vmTargetParams,
|
|
472
|
-
region: regionParam,
|
|
473
|
-
}, async ({ user, vmId, workspaceId, region }) => {
|
|
474
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
475
|
-
if (!t.ok)
|
|
476
|
-
return t.res;
|
|
477
|
-
try {
|
|
478
|
-
const result = await lds.sessionStart(t.target.url, user, t.target.auth);
|
|
479
|
-
return ok({
|
|
480
|
-
...result,
|
|
481
|
-
hint: `Session for '${user}' is up — target it with targetUser='${user}'.`,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
catch (e) {
|
|
485
|
-
return text(`Failed to start session for '${user}': ${e.message}`);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
server.tool("vm_stop_session", "Disconnect a registered user's session on the connected broker VM (POST /sessions/{user}/stop). The user STAYS logged in but the session goes dark and stays dark until an explicit start or a new job re-arms it; browser jobs keep working, desktop jobs are refused while dark. The keeper-host user cannot be stopped through MDS.", {
|
|
489
|
-
user: z.string().describe("Registered Windows account name (see vm_list_sessions)"),
|
|
490
|
-
...vmTargetParams,
|
|
491
|
-
region: regionParam,
|
|
492
|
-
}, async ({ user, vmId, workspaceId, region }) => {
|
|
493
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
494
|
-
if (!t.ok)
|
|
495
|
-
return t.res;
|
|
496
|
-
try {
|
|
497
|
-
const result = await lds.sessionStop(t.target.url, user, t.target.auth);
|
|
498
|
-
return ok(result);
|
|
499
|
-
}
|
|
500
|
-
catch (e) {
|
|
501
|
-
return text(`Failed to stop session for '${user}': ${e.message}`);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
117
|
// ── MDS configuration & service control (VM service, bearer) ──
|
|
505
118
|
server.tool("vm_get_mds_config", "Read a VM's MDS multi-session configuration and observed state from the VM service: registered robot users (with secretRef), keeper_enabled, session_resolution, live sessions, agent status, and restart/apply tracking. Workspace OWNER/ADMIN only (the service returns 403 otherwise). No VM connection needed — addresses the VM by workspaceId + vmId.", {
|
|
506
119
|
workspaceId: z.number().describe("Workspace ID"),
|
|
@@ -633,109 +246,6 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
633
246
|
return text(`Service ${action} failed: ${e.message}`);
|
|
634
247
|
}
|
|
635
248
|
});
|
|
636
|
-
// ── UI inspection ─────────────────────────────────────────
|
|
637
|
-
server.tool("vm_inspect_ui", "X-ray the VM screen: list windows, get element trees with coordinates and properties, find elements at a point, or get the focused element. Use element_tree to map all interactive elements before writing desktop automation scripts.", {
|
|
638
|
-
mode: z
|
|
639
|
-
.enum([
|
|
640
|
-
"window_list",
|
|
641
|
-
"screen_info",
|
|
642
|
-
"element_at_point",
|
|
643
|
-
"element_tree",
|
|
644
|
-
"focused_element",
|
|
645
|
-
])
|
|
646
|
-
.describe("Inspection mode"),
|
|
647
|
-
x: z.number().optional().describe("X coordinate (for element_at_point)"),
|
|
648
|
-
y: z.number().optional().describe("Y coordinate (for element_at_point)"),
|
|
649
|
-
windowTitle: z
|
|
650
|
-
.string()
|
|
651
|
-
.optional()
|
|
652
|
-
.describe("Window title filter (for element_tree)"),
|
|
653
|
-
framework: z
|
|
654
|
-
.enum(["auto", "uiautomation", "pywinauto", "jab"])
|
|
655
|
-
.optional()
|
|
656
|
-
.describe("Automation framework (default: auto → uiautomation)"),
|
|
657
|
-
depth: z
|
|
658
|
-
.number()
|
|
659
|
-
.optional()
|
|
660
|
-
.describe("Max tree depth (for element_tree, default 3)"),
|
|
661
|
-
targetUser: targetUserParam,
|
|
662
|
-
...vmTargetParams,
|
|
663
|
-
region: regionParam,
|
|
664
|
-
}, async ({ mode, x, y, windowTitle, framework, depth, targetUser, vmId, workspaceId, region }) => {
|
|
665
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
666
|
-
if (!t.ok)
|
|
667
|
-
return t.res;
|
|
668
|
-
const { target } = t;
|
|
669
|
-
const script = generateInspectScript({
|
|
670
|
-
mode,
|
|
671
|
-
x,
|
|
672
|
-
y,
|
|
673
|
-
windowTitle,
|
|
674
|
-
framework,
|
|
675
|
-
depth,
|
|
676
|
-
});
|
|
677
|
-
try {
|
|
678
|
-
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
679
|
-
if (!res.success) {
|
|
680
|
-
return text(`UI inspection failed (exit ${res.exitCode}): ${res.stderr || res.stdout}`);
|
|
681
|
-
}
|
|
682
|
-
try {
|
|
683
|
-
return ok(JSON.parse(res.stdout));
|
|
684
|
-
}
|
|
685
|
-
catch {
|
|
686
|
-
return text(res.stdout);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
catch (e) {
|
|
690
|
-
return text(`UI inspection failed: ${e.message}`);
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
// ── Clipboard ─────────────────────────────────────────────
|
|
694
|
-
server.tool("vm_read_clipboard", "Read the current clipboard contents from the VM", {
|
|
695
|
-
targetUser: targetUserParam,
|
|
696
|
-
...vmTargetParams,
|
|
697
|
-
region: regionParam,
|
|
698
|
-
}, async ({ targetUser, vmId, workspaceId, region }) => {
|
|
699
|
-
const t = await requireTarget({ vmId, workspaceId, region: region });
|
|
700
|
-
if (!t.ok)
|
|
701
|
-
return t.res;
|
|
702
|
-
const { target } = t;
|
|
703
|
-
const script = `
|
|
704
|
-
import json
|
|
705
|
-
|
|
706
|
-
clipboard_text = ""
|
|
707
|
-
try:
|
|
708
|
-
import win32clipboard
|
|
709
|
-
win32clipboard.OpenClipboard()
|
|
710
|
-
try:
|
|
711
|
-
clipboard_text = win32clipboard.GetClipboardData()
|
|
712
|
-
except TypeError:
|
|
713
|
-
clipboard_text = ""
|
|
714
|
-
finally:
|
|
715
|
-
win32clipboard.CloseClipboard()
|
|
716
|
-
except ImportError:
|
|
717
|
-
import subprocess
|
|
718
|
-
result = subprocess.run(["powershell", "-command", "Get-Clipboard"], capture_output=True, text=True)
|
|
719
|
-
clipboard_text = result.stdout.strip()
|
|
720
|
-
|
|
721
|
-
print(json.dumps({"clipboard": clipboard_text}))
|
|
722
|
-
`.trim();
|
|
723
|
-
try {
|
|
724
|
-
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
725
|
-
if (!res.success) {
|
|
726
|
-
return text(`Clipboard read failed: ${res.stderr || "unknown error"}`);
|
|
727
|
-
}
|
|
728
|
-
try {
|
|
729
|
-
return ok(JSON.parse(res.stdout));
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
return text(res.stdout);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
catch (e) {
|
|
736
|
-
return text(`Clipboard read failed: ${e.message}`);
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
249
|
// ── MDS Installation ───────────────────────────────────────
|
|
740
250
|
/**
|
|
741
251
|
* Refuse legacy standalone management actions on broker VMs (live /health
|