@minicor/mcp-server 4.7.0 → 4.8.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/README.md +18 -7
- package/dist/__tests__/config-stores.test.js +8 -7
- package/dist/__tests__/config-stores.test.js.map +1 -1
- package/dist/__tests__/dev-mode-draft.test.js +9 -10
- package/dist/__tests__/dev-mode-draft.test.js.map +1 -1
- package/dist/__tests__/discovery.test.js +6 -5
- package/dist/__tests__/discovery.test.js.map +1 -1
- package/dist/__tests__/helpers.test.js +20 -0
- package/dist/__tests__/helpers.test.js.map +1 -1
- package/dist/__tests__/http-server.test.js +118 -0
- package/dist/__tests__/http-server.test.js.map +1 -1
- package/dist/__tests__/mds-errors.test.d.ts +2 -0
- package/dist/__tests__/mds-errors.test.d.ts.map +1 -0
- package/dist/__tests__/mds-errors.test.js +91 -0
- package/dist/__tests__/mds-errors.test.js.map +1 -0
- package/dist/__tests__/state.test.js +104 -44
- package/dist/__tests__/state.test.js.map +1 -1
- package/dist/__tests__/vm-broker.test.d.ts +2 -0
- package/dist/__tests__/vm-broker.test.d.ts.map +1 -0
- package/dist/__tests__/vm-broker.test.js +812 -0
- package/dist/__tests__/vm-broker.test.js.map +1 -0
- package/dist/__tests__/vm-management.test.d.ts +2 -0
- package/dist/__tests__/vm-management.test.d.ts.map +1 -0
- package/dist/__tests__/vm-management.test.js +507 -0
- package/dist/__tests__/vm-management.test.js.map +1 -0
- package/dist/agent-service-client.d.ts +12 -0
- package/dist/agent-service-client.d.ts.map +1 -1
- package/dist/agent-service-client.js +74 -0
- package/dist/agent-service-client.js.map +1 -1
- package/dist/helpers.d.ts +14 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +12 -3
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lds-client.d.ts +59 -10
- package/dist/lds-client.d.ts.map +1 -1
- package/dist/lds-client.js +53 -9
- package/dist/lds-client.js.map +1 -1
- package/dist/lib.d.ts +6 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +5 -1
- package/dist/lib.js.map +1 -1
- package/dist/mds-errors.d.ts +32 -0
- package/dist/mds-errors.d.ts.map +1 -0
- package/dist/mds-errors.js +61 -0
- package/dist/mds-errors.js.map +1 -0
- package/dist/server-surface.d.ts +2 -0
- 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/state.d.ts +28 -14
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +58 -40
- package/dist/state.js.map +1 -1
- package/dist/tools/agents.d.ts.map +1 -1
- package/dist/tools/agents.js +2 -3
- package/dist/tools/agents.js.map +1 -1
- package/dist/tools/config-stores.d.ts.map +1 -1
- package/dist/tools/config-stores.js +11 -12
- package/dist/tools/config-stores.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +1 -2
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/core.d.ts.map +1 -1
- package/dist/tools/core.js +2 -3
- package/dist/tools/core.js.map +1 -1
- package/dist/tools/dev-mode-draft.d.ts.map +1 -1
- package/dist/tools/dev-mode-draft.js +16 -17
- package/dist/tools/dev-mode-draft.js.map +1 -1
- package/dist/tools/discovery.d.ts.map +1 -1
- package/dist/tools/discovery.js +7 -8
- package/dist/tools/discovery.js.map +1 -1
- package/dist/tools/issues.d.ts.map +1 -1
- package/dist/tools/issues.js +7 -8
- package/dist/tools/issues.js.map +1 -1
- package/dist/tools/jobs.d.ts.map +1 -1
- package/dist/tools/jobs.js +1 -2
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/session.d.ts.map +1 -1
- package/dist/tools/session.js +9 -10
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/skills.d.ts.map +1 -1
- package/dist/tools/skills.js +2 -3
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/sync-tools.d.ts.map +1 -1
- package/dist/tools/sync-tools.js +2 -3
- package/dist/tools/sync-tools.js.map +1 -1
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +51 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/tfa.d.ts.map +1 -1
- package/dist/tools/tfa.js +14 -5
- package/dist/tools/tfa.js.map +1 -1
- package/dist/tools/vm-rpa.d.ts +2 -1
- package/dist/tools/vm-rpa.d.ts.map +1 -1
- package/dist/tools/vm-rpa.js +82 -38
- package/dist/tools/vm-rpa.js.map +1 -1
- package/dist/tools/vm.d.ts +2 -1
- package/dist/tools/vm.d.ts.map +1 -1
- package/dist/tools/vm.js +511 -114
- package/dist/tools/vm.js.map +1 -1
- package/dist/tools/workflow-ops.d.ts.map +1 -1
- package/dist/tools/workflow-ops.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vm-service-client.d.ts +8 -2
- package/dist/vm-service-client.d.ts.map +1 -1
- package/dist/vm-service-client.js.map +1 -1
- package/dist/vm-target.d.ts +55 -0
- package/dist/vm-target.d.ts.map +1 -0
- package/dist/vm-target.js +134 -0
- package/dist/vm-target.js.map +1 -0
- package/package.json +1 -1
package/dist/tools/vm.js
CHANGED
|
@@ -1,20 +1,54 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ok, text, json,
|
|
3
|
-
import { ldsConnection, setLdsConnection, ldsAuth, getCachedVm, getWorkspaceRegion } from "../state.js";
|
|
2
|
+
import { ok, text, json, DIRECT_VM_TOOLS_DISABLED_HOSTED, } from "../helpers.js";
|
|
4
3
|
import * as lds from "../lds-client.js";
|
|
5
4
|
import { generateInspectScript } from "../inspect-scripts.js";
|
|
5
|
+
import { fetchVmCredentials, fetchVmCredentialsSoft, resolveVmTarget, vmTargetParams, } from "../vm-target.js";
|
|
6
6
|
import { registerVmDiscovery } from "./discovery.js";
|
|
7
7
|
const regionParam = z
|
|
8
8
|
.enum(["us", "ca"])
|
|
9
9
|
.optional()
|
|
10
10
|
.describe("Region (us or ca). Auto-detected from workspace if omitted.");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
/** A robot-user catalog entry as accepted by the VM service (MDSRegisteredUserInput). */
|
|
16
|
+
const mdsRegisteredUserSchema = z.object({
|
|
17
|
+
username: z.string().describe("Local Windows account name"),
|
|
18
|
+
password: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Raw RDP password — write-only. The VM service exchanges it for a Secret Manager reference on save; it is never stored and never echoed back. Supply this or secretRef."),
|
|
22
|
+
secretRef: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Existing Google Secret Manager reference for the RDP password"),
|
|
26
|
+
resolution: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Per-user session resolution override, e.g. '1920x1080'"),
|
|
30
|
+
});
|
|
31
|
+
/** @param directVmTools false on the hosted stateless server — tools stay registered but point to a local install. */
|
|
32
|
+
export function register(deps, directVmTools = true) {
|
|
33
|
+
const { server, state } = deps;
|
|
34
|
+
function requireDirect() {
|
|
35
|
+
if (!directVmTools)
|
|
36
|
+
return text(DIRECT_VM_TOOLS_DISABLED_HOSTED);
|
|
37
|
+
return null;
|
|
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
|
+
}
|
|
18
52
|
// ── VM discovery ─────────────────────────────────────────────
|
|
19
53
|
registerVmDiscovery(deps);
|
|
20
54
|
// ── VM deployment ──────────────────────────────────────────
|
|
@@ -25,11 +59,23 @@ export function register(deps) {
|
|
|
25
59
|
region: regionParam,
|
|
26
60
|
location: z.enum(["us", "canada", "europe"]).default("us").describe("Geography — auto-selects best region by quota"),
|
|
27
61
|
vmSize: z.string().default("Standard_D2s_v3").describe("Azure VM size"),
|
|
28
|
-
vmNamePrefix: z
|
|
62
|
+
vmNamePrefix: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe("VM name prefix — REQUIRED by the VM service (max 15 chars, truncated if longer; a uniqueness suffix is appended)"),
|
|
29
65
|
enableRecording: z.boolean().default(true),
|
|
30
|
-
|
|
66
|
+
rdpWidth: z.number().optional().describe("Preferred RDP desktop width"),
|
|
67
|
+
rdpHeight: z.number().optional().describe("Preferred RDP desktop height"),
|
|
68
|
+
mdsKeeperEnabled: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.default(true)
|
|
71
|
+
.describe("Whether MDS signs robot users in automatically (multi-session keeper)"),
|
|
72
|
+
mdsRegisteredUsers: z
|
|
73
|
+
.array(mdsRegisteredUserSchema)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Robot user catalog to seed at creation (multi-session VMs). Passwords are exchanged for Secret Manager references and never stored."),
|
|
76
|
+
}, async ({ workspaceId, region, location, vmSize, vmNamePrefix, enableRecording, rdpWidth, rdpHeight, mdsKeeperEnabled, mdsRegisteredUsers }) => {
|
|
31
77
|
try {
|
|
32
|
-
const wsRegion = region ?? getWorkspaceRegion(workspaceId);
|
|
78
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
33
79
|
const authToken = deps.getAuthToken?.(wsRegion);
|
|
34
80
|
if (!authToken)
|
|
35
81
|
return text("No auth token available. Please re-authenticate.");
|
|
@@ -44,12 +90,17 @@ export function register(deps) {
|
|
|
44
90
|
workspace_id: String(workspaceId),
|
|
45
91
|
location,
|
|
46
92
|
vm_size: vmSize,
|
|
47
|
-
vm_name_prefix: vmNamePrefix
|
|
93
|
+
vm_name_prefix: vmNamePrefix,
|
|
48
94
|
exposed_ports: [1016, 3389],
|
|
49
95
|
enable_recording: enableRecording,
|
|
50
96
|
enable_queue_and_lock: false,
|
|
51
|
-
install_laminar_service: false,
|
|
52
97
|
windows_version: "2022-datacenter",
|
|
98
|
+
...(rdpWidth != null ? { rdp_width: rdpWidth } : {}),
|
|
99
|
+
...(rdpHeight != null ? { rdp_height: rdpHeight } : {}),
|
|
100
|
+
mds_keeper_enabled: mdsKeeperEnabled,
|
|
101
|
+
...(mdsRegisteredUsers?.length
|
|
102
|
+
? { mds_registered_users: mdsRegisteredUsers }
|
|
103
|
+
: {}),
|
|
53
104
|
}),
|
|
54
105
|
});
|
|
55
106
|
if (!res.ok) {
|
|
@@ -57,11 +108,13 @@ export function register(deps) {
|
|
|
57
108
|
return text(`VM deployment failed (HTTP ${res.status}): ${errBody}`);
|
|
58
109
|
}
|
|
59
110
|
const result = await res.json();
|
|
111
|
+
// Deploy response nests the admin logon under `credentials`.
|
|
60
112
|
return ok({
|
|
61
113
|
message: "VM deployment started. It takes ~5-10 minutes.",
|
|
62
114
|
vm_name: result.vm_name,
|
|
63
|
-
|
|
64
|
-
|
|
115
|
+
status: result.status,
|
|
116
|
+
username: result.credentials?.username,
|
|
117
|
+
password: result.credentials?.password,
|
|
65
118
|
next_steps: [
|
|
66
119
|
"Poll vm_list to check when deployment_status becomes 'deployed'",
|
|
67
120
|
"Once deployed, use install_mds_on_vm to install MDS",
|
|
@@ -74,53 +127,113 @@ export function register(deps) {
|
|
|
74
127
|
}
|
|
75
128
|
});
|
|
76
129
|
// ── Connection management ─────────────────────────────────
|
|
77
|
-
server.tool("vm_connect", "Connect to a VM. Provide vmId (from vm_list) to auto-fill credentials, or provide url directly.
|
|
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.", {
|
|
78
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."),
|
|
79
136
|
url: z.string().optional().describe("MDS base URL (Cloudflare Tunnel) — use this OR vmId"),
|
|
80
137
|
apiKey: z.string().optional().describe("MDS API key (not needed with vmId)"),
|
|
81
138
|
serviceId: z.string().optional().describe("MDS service ID (not needed with vmId)"),
|
|
82
|
-
|
|
139
|
+
region: regionParam,
|
|
140
|
+
}, async ({ vmId, workspaceId, url, apiKey, serviceId, region }) => {
|
|
141
|
+
const disabled = requireDirect();
|
|
142
|
+
if (disabled)
|
|
143
|
+
return disabled;
|
|
83
144
|
let connectUrl = url;
|
|
84
145
|
let connectApiKey = apiKey;
|
|
85
146
|
let connectServiceId = serviceId;
|
|
86
147
|
let vmName;
|
|
87
148
|
if (vmId) {
|
|
88
|
-
const vm = getCachedVm(vmId);
|
|
89
|
-
if (
|
|
90
|
-
|
|
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;
|
|
91
180
|
}
|
|
92
|
-
|
|
93
|
-
return text(
|
|
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.");
|
|
94
183
|
}
|
|
95
|
-
connectUrl = vm.tunnel_url;
|
|
96
|
-
connectApiKey = vm.api_key;
|
|
97
|
-
connectServiceId = vm.service_id;
|
|
98
|
-
vmName = vm.vm_name;
|
|
99
184
|
}
|
|
100
185
|
if (!connectUrl) {
|
|
101
186
|
return text("Provide either vmId (from vm_list) or url to connect.");
|
|
102
187
|
}
|
|
103
188
|
try {
|
|
104
189
|
const health = await lds.health(connectUrl);
|
|
105
|
-
|
|
190
|
+
const mode = lds.modeFromHealth(health);
|
|
191
|
+
state.setLdsConnection({
|
|
106
192
|
url: connectUrl,
|
|
107
193
|
apiKey: connectApiKey,
|
|
108
194
|
serviceId: connectServiceId,
|
|
109
195
|
vmId,
|
|
110
196
|
vmName,
|
|
197
|
+
mode,
|
|
111
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
|
+
}
|
|
112
210
|
return ok({
|
|
113
211
|
connected: true,
|
|
114
212
|
url: connectUrl,
|
|
115
213
|
vmId,
|
|
116
214
|
vmName,
|
|
215
|
+
mode,
|
|
117
216
|
authenticated: !!(connectApiKey && connectServiceId),
|
|
118
217
|
service: health,
|
|
218
|
+
...(registeredUsers ? { registeredUsers } : {}),
|
|
119
219
|
next_steps: [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
: []),
|
|
124
237
|
],
|
|
125
238
|
automation_approaches: {
|
|
126
239
|
browser: "CDP via Python websocket to Chrome on port 9222 → vm_execute_script",
|
|
@@ -134,20 +247,31 @@ export function register(deps) {
|
|
|
134
247
|
}
|
|
135
248
|
});
|
|
136
249
|
server.tool("vm_disconnect", "Disconnect from the current VM", {}, async () => {
|
|
137
|
-
|
|
250
|
+
const disabled = requireDirect();
|
|
251
|
+
if (disabled)
|
|
252
|
+
return disabled;
|
|
253
|
+
state.setLdsConnection(null);
|
|
138
254
|
return text("Disconnected from VM.");
|
|
139
255
|
});
|
|
140
|
-
server.tool("vm_status", "Check health/status of
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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;
|
|
144
264
|
try {
|
|
145
|
-
const health = await lds.health(
|
|
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);
|
|
146
269
|
return ok({
|
|
147
270
|
connected: true,
|
|
148
|
-
url:
|
|
149
|
-
vmId:
|
|
150
|
-
vmName:
|
|
271
|
+
url: target.url,
|
|
272
|
+
vmId: target.vmId,
|
|
273
|
+
vmName: target.vmName,
|
|
274
|
+
mode,
|
|
151
275
|
...health,
|
|
152
276
|
});
|
|
153
277
|
}
|
|
@@ -156,12 +280,17 @@ export function register(deps) {
|
|
|
156
280
|
}
|
|
157
281
|
});
|
|
158
282
|
// ── Screenshot ────────────────────────────────────────────
|
|
159
|
-
server.tool("vm_screenshot", "Take a full-screen screenshot of the
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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;
|
|
163
292
|
try {
|
|
164
|
-
const res = await lds.screenshot(
|
|
293
|
+
const res = await lds.screenshot(target.url, target.auth, targetUser);
|
|
165
294
|
const { width, height, size_bytes, capture_duration_ms, timestamp } = res.metadata;
|
|
166
295
|
return {
|
|
167
296
|
content: [
|
|
@@ -192,10 +321,17 @@ export function register(deps) {
|
|
|
192
321
|
y: z.number().describe("Top Y coordinate"),
|
|
193
322
|
width: z.number().describe("Region width in pixels"),
|
|
194
323
|
height: z.number().describe("Region height in pixels"),
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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;
|
|
199
335
|
const x2 = x + width;
|
|
200
336
|
const y2 = y + height;
|
|
201
337
|
const script = `
|
|
@@ -209,7 +345,7 @@ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
|
209
345
|
print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
210
346
|
`.trim();
|
|
211
347
|
try {
|
|
212
|
-
const res = await lds.execute(
|
|
348
|
+
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
213
349
|
if (!res.success) {
|
|
214
350
|
return text(`Region screenshot failed: ${res.stderr || "unknown error"}`);
|
|
215
351
|
}
|
|
@@ -237,16 +373,20 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
237
373
|
}
|
|
238
374
|
});
|
|
239
375
|
// ── Script execution ──────────────────────────────────────
|
|
240
|
-
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.", {
|
|
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.", {
|
|
241
377
|
script: z.string().describe("Python script to execute"),
|
|
242
378
|
executionId: z.string().optional().describe("Execution ID for tracking"),
|
|
243
379
|
flowId: z.string().optional().describe("Flow ID for tracking"),
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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;
|
|
248
388
|
try {
|
|
249
|
-
const res = await lds.execute(
|
|
389
|
+
const res = await lds.execute(target.url, script, { executionId, flowId, targetUser }, target.auth);
|
|
250
390
|
return ok({
|
|
251
391
|
success: res.success,
|
|
252
392
|
exitCode: res.exitCode,
|
|
@@ -256,39 +396,241 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
256
396
|
skipped: res.skipped,
|
|
257
397
|
stopped: res.stopped,
|
|
258
398
|
resultData: res.resultData,
|
|
399
|
+
// Broker attribution (absent on standalone).
|
|
400
|
+
...(res.targetUser !== undefined ? { targetUser: res.targetUser } : {}),
|
|
259
401
|
});
|
|
260
402
|
}
|
|
261
403
|
catch (e) {
|
|
262
404
|
return text(`Execution failed: ${e.message}`);
|
|
263
405
|
}
|
|
264
406
|
});
|
|
265
|
-
// ── Execution status
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
270
422
|
try {
|
|
271
|
-
|
|
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);
|
|
272
434
|
return ok(status);
|
|
273
435
|
}
|
|
274
436
|
catch (e) {
|
|
275
437
|
return text(`Failed to get execution status: ${e.message}`);
|
|
276
438
|
}
|
|
277
439
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}, async ({
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
285
|
-
return
|
|
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;
|
|
286
496
|
try {
|
|
287
|
-
const result = await lds.
|
|
497
|
+
const result = await lds.sessionStop(t.target.url, user, t.target.auth);
|
|
288
498
|
return ok(result);
|
|
289
499
|
}
|
|
290
500
|
catch (e) {
|
|
291
|
-
return text(`
|
|
501
|
+
return text(`Failed to stop session for '${user}': ${e.message}`);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
// ── MDS configuration & service control (VM service, bearer) ──
|
|
505
|
+
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
|
+
workspaceId: z.number().describe("Workspace ID"),
|
|
507
|
+
vmId: z.number().describe("VM ID from vm_list"),
|
|
508
|
+
region: regionParam,
|
|
509
|
+
}, async ({ workspaceId, vmId, region }) => {
|
|
510
|
+
try {
|
|
511
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
512
|
+
const authToken = deps.getAuthToken?.(wsRegion);
|
|
513
|
+
if (!authToken)
|
|
514
|
+
return text("No auth token available. Please re-authenticate.");
|
|
515
|
+
const res = await fetch(`${deps.getVmServiceBase()}/workspaces/${workspaceId}/vms/${vmId}/mds-config`, { headers: { "Authorization": `Bearer ${authToken}` } });
|
|
516
|
+
if (!res.ok) {
|
|
517
|
+
const errBody = await res.text().catch(() => "");
|
|
518
|
+
if (res.status === 403) {
|
|
519
|
+
return text(`MDS config is restricted to workspace OWNER/ADMIN (HTTP 403): ${errBody}`);
|
|
520
|
+
}
|
|
521
|
+
return text(`Failed to get MDS config (HTTP ${res.status}): ${errBody}`);
|
|
522
|
+
}
|
|
523
|
+
return ok(await res.json());
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
return text(`Failed to get MDS config: ${e.message}`);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
server.tool("vm_update_mds_config", "Replace a VM's MDS multi-session configuration via the VM service. registeredUsers is a FULL REPLACE of the robot-user catalog — read vm_get_mds_config first and resend every user you want to keep (existing users keep working when you resend their secretRef unchanged). New or rotated users may carry a raw password: it is exchanged for a Secret Manager reference before saving and never stored or echoed back. Saving ALWAYS queues the MDS restart that applies the config (on broker VMs only the MinicorBroker task restarts — agents and robot sessions keep running); the edit stays 'pending' until the VM next fetches its config. Workspace OWNER/ADMIN only.", {
|
|
530
|
+
workspaceId: z.number().describe("Workspace ID"),
|
|
531
|
+
vmId: z.number().describe("VM ID from vm_list"),
|
|
532
|
+
registeredUsers: z
|
|
533
|
+
.array(mdsRegisteredUserSchema)
|
|
534
|
+
.describe("The complete robot-user catalog (full replace — omitted users are removed). Each entry needs secretRef or a write-only password."),
|
|
535
|
+
keeperEnabled: z
|
|
536
|
+
.boolean()
|
|
537
|
+
.describe("Whether MDS signs robot users in automatically (gates vm_start_session and on-demand logons)"),
|
|
538
|
+
sessionResolution: z
|
|
539
|
+
.string()
|
|
540
|
+
.optional()
|
|
541
|
+
.describe("VM-level session resolution 'WxH' (e.g. '1920x1080'); omit to keep the stored value"),
|
|
542
|
+
region: regionParam,
|
|
543
|
+
}, async ({ workspaceId, vmId, registeredUsers, keeperEnabled, sessionResolution, region }) => {
|
|
544
|
+
try {
|
|
545
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
546
|
+
const authToken = deps.getAuthToken?.(wsRegion);
|
|
547
|
+
if (!authToken)
|
|
548
|
+
return text("No auth token available. Please re-authenticate.");
|
|
549
|
+
const res = await fetch(`${deps.getVmServiceBase()}/workspaces/${workspaceId}/vms/${vmId}/mds-config`, {
|
|
550
|
+
method: "PUT",
|
|
551
|
+
headers: {
|
|
552
|
+
"Content-Type": "application/json",
|
|
553
|
+
"Authorization": `Bearer ${authToken}`,
|
|
554
|
+
},
|
|
555
|
+
body: JSON.stringify({
|
|
556
|
+
registered_users: registeredUsers,
|
|
557
|
+
keeper_enabled: keeperEnabled,
|
|
558
|
+
...(sessionResolution !== undefined
|
|
559
|
+
? { session_resolution: sessionResolution }
|
|
560
|
+
: {}),
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
if (!res.ok) {
|
|
564
|
+
const errBody = await res.text().catch(() => "");
|
|
565
|
+
if (res.status === 403) {
|
|
566
|
+
return text(`MDS config is restricted to workspace OWNER/ADMIN (HTTP 403): ${errBody}`);
|
|
567
|
+
}
|
|
568
|
+
if (res.status === 502) {
|
|
569
|
+
return text(`Password exchange failed — the stored config is untouched (HTTP 502): ${errBody}`);
|
|
570
|
+
}
|
|
571
|
+
return text(`Failed to update MDS config (HTTP ${res.status}): ${errBody}`);
|
|
572
|
+
}
|
|
573
|
+
const config = await res.json();
|
|
574
|
+
return ok({
|
|
575
|
+
...config,
|
|
576
|
+
hint: "Saved. The MDS restart that applies this config is queued; restart_status/config_applied above track it. Check vm_get_mds_config (or vm_list_sessions once connected) to confirm the VM picked it up.",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
return text(`Failed to update MDS config: ${e.message}`);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
server.tool("vm_service_control", "Control the Minicor Desktop Service on a workspace VM through the VM service (no VM connection needed). The service routes on the VM's observed install: on broker/multi-session VMs 'restart' queues a MinicorBroker-only restart applied on the next heartbeat (agents and robot sessions keep running; this also applies pending MDS config) and other actions come back manual_required; legacy standalone installs get the full start/stop/restart/redeploy flows via Azure Run Command.", {
|
|
584
|
+
workspaceId: z.number().describe("Workspace ID"),
|
|
585
|
+
vmId: z.number().describe("VM ID from vm_list"),
|
|
586
|
+
action: z
|
|
587
|
+
.enum(["start", "stop", "restart", "status", "redeploy"])
|
|
588
|
+
.describe("Service action — 'restart' is the only remote action on broker VMs"),
|
|
589
|
+
serviceVersion: z
|
|
590
|
+
.string()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe("Specific MDS version to deploy (redeploy only)"),
|
|
593
|
+
enableRecording: z.boolean().optional().describe("Recording flag (redeploy only)"),
|
|
594
|
+
enableQueueAndLock: z.boolean().optional().describe("Queue-and-lock flag (redeploy only)"),
|
|
595
|
+
region: regionParam,
|
|
596
|
+
}, async ({ workspaceId, vmId, action, serviceVersion, enableRecording, enableQueueAndLock, region }) => {
|
|
597
|
+
try {
|
|
598
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
599
|
+
const authToken = deps.getAuthToken?.(wsRegion);
|
|
600
|
+
if (!authToken)
|
|
601
|
+
return text("No auth token available. Please re-authenticate.");
|
|
602
|
+
const redeployBody = action === "redeploy" &&
|
|
603
|
+
(serviceVersion !== undefined ||
|
|
604
|
+
enableRecording !== undefined ||
|
|
605
|
+
enableQueueAndLock !== undefined)
|
|
606
|
+
? {
|
|
607
|
+
...(serviceVersion !== undefined
|
|
608
|
+
? { laminar_service_version: serviceVersion }
|
|
609
|
+
: {}),
|
|
610
|
+
...(enableRecording !== undefined
|
|
611
|
+
? { enable_recording: enableRecording }
|
|
612
|
+
: {}),
|
|
613
|
+
...(enableQueueAndLock !== undefined
|
|
614
|
+
? { enable_queue_and_lock: enableQueueAndLock }
|
|
615
|
+
: {}),
|
|
616
|
+
}
|
|
617
|
+
: undefined;
|
|
618
|
+
const res = await fetch(`${deps.getVmServiceBase()}/workspaces/${workspaceId}/vms/${vmId}/service/${action}`, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: {
|
|
621
|
+
"Content-Type": "application/json",
|
|
622
|
+
"Authorization": `Bearer ${authToken}`,
|
|
623
|
+
},
|
|
624
|
+
...(redeployBody ? { body: JSON.stringify(redeployBody) } : {}),
|
|
625
|
+
});
|
|
626
|
+
if (!res.ok) {
|
|
627
|
+
const errBody = await res.text().catch(() => "");
|
|
628
|
+
return text(`Service ${action} failed (HTTP ${res.status}): ${errBody}`);
|
|
629
|
+
}
|
|
630
|
+
return ok(await res.json());
|
|
631
|
+
}
|
|
632
|
+
catch (e) {
|
|
633
|
+
return text(`Service ${action} failed: ${e.message}`);
|
|
292
634
|
}
|
|
293
635
|
});
|
|
294
636
|
// ── UI inspection ─────────────────────────────────────────
|
|
@@ -316,10 +658,14 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
316
658
|
.number()
|
|
317
659
|
.optional()
|
|
318
660
|
.describe("Max tree depth (for element_tree, default 3)"),
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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;
|
|
323
669
|
const script = generateInspectScript({
|
|
324
670
|
mode,
|
|
325
671
|
x,
|
|
@@ -329,7 +675,7 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
329
675
|
depth,
|
|
330
676
|
});
|
|
331
677
|
try {
|
|
332
|
-
const res = await lds.execute(
|
|
678
|
+
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
333
679
|
if (!res.success) {
|
|
334
680
|
return text(`UI inspection failed (exit ${res.exitCode}): ${res.stderr || res.stdout}`);
|
|
335
681
|
}
|
|
@@ -345,10 +691,15 @@ print(json.dumps({"image": b64, "width": img.width, "height": img.height}))
|
|
|
345
691
|
}
|
|
346
692
|
});
|
|
347
693
|
// ── Clipboard ─────────────────────────────────────────────
|
|
348
|
-
server.tool("vm_read_clipboard", "Read the current clipboard contents from the VM", {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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;
|
|
352
703
|
const script = `
|
|
353
704
|
import json
|
|
354
705
|
|
|
@@ -370,7 +721,7 @@ except ImportError:
|
|
|
370
721
|
print(json.dumps({"clipboard": clipboard_text}))
|
|
371
722
|
`.trim();
|
|
372
723
|
try {
|
|
373
|
-
const res = await lds.execute(
|
|
724
|
+
const res = await lds.execute(target.url, script, { targetUser }, target.auth);
|
|
374
725
|
if (!res.success) {
|
|
375
726
|
return text(`Clipboard read failed: ${res.stderr || "unknown error"}`);
|
|
376
727
|
}
|
|
@@ -386,28 +737,63 @@ print(json.dumps({"clipboard": clipboard_text}))
|
|
|
386
737
|
}
|
|
387
738
|
});
|
|
388
739
|
// ── MDS Installation ───────────────────────────────────────
|
|
740
|
+
/**
|
|
741
|
+
* Refuse legacy standalone management actions on broker VMs (live /health
|
|
742
|
+
* probe). Deliberately fails open when MDS is unreachable — being down is
|
|
743
|
+
* the normal reason to (re)install or (re)start.
|
|
744
|
+
*/
|
|
745
|
+
async function refuseLegacyStandaloneAction(tunnelUrl, action, consequence) {
|
|
746
|
+
if (!tunnelUrl)
|
|
747
|
+
return null;
|
|
748
|
+
let mode;
|
|
749
|
+
try {
|
|
750
|
+
mode = await lds.discoverMode(tunnelUrl);
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return null; // MDS down/unreachable — proceed with the legacy action
|
|
754
|
+
}
|
|
755
|
+
if (mode === "broker") {
|
|
756
|
+
return text(`Refusing ${action}: this VM runs MDS in broker (multi-session) mode, and ${action} is a legacy standalone action — ${consequence} ` +
|
|
757
|
+
`Use the mode-aware restart instead: vm_service_control with action 'restart' restarts only the MinicorBroker scheduled task (agents keep running and re-register), and saving the VM's MDS configuration (vm_update_mds_config) triggers that same restart. ` +
|
|
758
|
+
`See vm_list_sessions for the broker's current users/agents.`);
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
389
762
|
const INSTALL_SCRIPT_URL = "https://www.minicor.com/installs/desktop-service/ps1";
|
|
390
763
|
/** Escape for single-quoted PowerShell strings (only ' needs doubling). */
|
|
391
764
|
const psEscape = (s) => s.replace(/'/g, "''");
|
|
392
765
|
server.tool("get_mds_install_command", "Generate a pre-filled PowerShell one-liner to install Minicor Desktop Service on a VM. " +
|
|
393
766
|
"Returns a command the user can paste into an elevated PowerShell window on the target machine.", {
|
|
394
767
|
vmId: z.number().optional().describe("VM ID from vm_list — auto-fills serviceId and apiKey"),
|
|
768
|
+
workspaceId: z
|
|
769
|
+
.number()
|
|
770
|
+
.optional()
|
|
771
|
+
.describe("Workspace that owns the VM. With vmId, resolves credentials straight from the VM service — no prior vm_list needed."),
|
|
395
772
|
serviceId: z.string().optional().describe("MDS service ID (manual override)"),
|
|
396
773
|
apiKey: z.string().optional().describe("MDS API key (manual override)"),
|
|
397
774
|
enableRecording: z.boolean().default(true),
|
|
398
775
|
resolution: z.string().default("1280x720"),
|
|
399
776
|
installerUrl: z.string().optional().describe("Override installer URL (e.g. ngrok URL for testing)"),
|
|
400
|
-
|
|
777
|
+
region: regionParam,
|
|
778
|
+
}, async ({ vmId, workspaceId, serviceId, apiKey, enableRecording, resolution, installerUrl, region }) => {
|
|
401
779
|
let sid = serviceId;
|
|
402
780
|
let key = apiKey;
|
|
403
781
|
if (vmId) {
|
|
404
|
-
const vm = getCachedVm(vmId);
|
|
782
|
+
const vm = state.getCachedVm(vmId);
|
|
405
783
|
if (vm) {
|
|
406
|
-
sid = sid || vm.service_id;
|
|
407
|
-
key = key || vm.api_key;
|
|
784
|
+
sid = sid || (vm.service_id ?? undefined);
|
|
785
|
+
key = key || (vm.api_key ?? undefined);
|
|
786
|
+
}
|
|
787
|
+
else if (workspaceId !== undefined) {
|
|
788
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
789
|
+
const resolved = await fetchVmCredentials(deps, workspaceId, vmId, wsRegion);
|
|
790
|
+
if (!resolved.ok)
|
|
791
|
+
return text(resolved.message);
|
|
792
|
+
sid = sid || resolved.creds.serviceId;
|
|
793
|
+
key = key || resolved.creds.apiKey;
|
|
408
794
|
}
|
|
409
795
|
else {
|
|
410
|
-
return text("VM not found in cache. Run vm_list first to discover available VMs.");
|
|
796
|
+
return text("VM not found in cache. Run vm_list first to discover available VMs, or pass workspaceId alongside vmId to resolve it directly.");
|
|
411
797
|
}
|
|
412
798
|
}
|
|
413
799
|
const envVars = [];
|
|
@@ -441,28 +827,28 @@ print(json.dumps({"clipboard": clipboard_text}))
|
|
|
441
827
|
resolution: z.string().default("1280x720"),
|
|
442
828
|
installerUrl: z.string().optional().describe("Override installer URL (e.g. ngrok URL for testing)"),
|
|
443
829
|
}, async ({ workspaceId, vmId, region, enableRecording, resolution, installerUrl }, extra) => {
|
|
444
|
-
const
|
|
445
|
-
if (
|
|
446
|
-
return
|
|
830
|
+
const disabled = requireDirect();
|
|
831
|
+
if (disabled)
|
|
832
|
+
return disabled;
|
|
833
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
834
|
+
const vm = state.getCachedVm(vmId);
|
|
835
|
+
// Resolved up front so the mode probe below still has a tunnel URL.
|
|
836
|
+
const coldCreds = vm
|
|
837
|
+
? undefined
|
|
838
|
+
: await fetchVmCredentialsSoft(deps, workspaceId, vmId, wsRegion);
|
|
839
|
+
if (coldCreds && !coldCreds.tunnelUrl && !coldCreds.apiKey) {
|
|
840
|
+
return text(`VM ${vmId} not found: it is not in the vm_list cache, and the VM service could not resolve it in workspace ${workspaceId}.`);
|
|
447
841
|
}
|
|
842
|
+
const refusal = await refuseLegacyStandaloneAction(vm?.tunnel_url ?? coldCreds?.tunnelUrl, "install_mds_on_vm", "the standalone installer replaces the broker/agent scheduled-task topology (a manual /MULTISESSION=1 install is the broker path).");
|
|
843
|
+
if (refusal)
|
|
844
|
+
return refusal;
|
|
448
845
|
const scriptUrl = installerUrl || INSTALL_SCRIPT_URL;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const authToken = deps.getAuthToken?.(wsRegion);
|
|
456
|
-
if (authToken) {
|
|
457
|
-
const credRes = await fetch(`${deps.getVmServiceBase()}/workspaces/${workspaceId}/vms/${vmId}/credentials`, { headers: { "Authorization": `Bearer ${authToken}` } });
|
|
458
|
-
if (credRes.ok) {
|
|
459
|
-
const creds = await credRes.json();
|
|
460
|
-
autoLogonUser = creds.admin_username || "";
|
|
461
|
-
autoLogonPass = creds.admin_password || "";
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
catch { /* credentials fetch failed, skip auto-logon */ }
|
|
846
|
+
// Fetch VM credentials for auto-logon (ensures active desktop session
|
|
847
|
+
// for RPA) and as the API-key fallback when the list was redacted.
|
|
848
|
+
const creds = coldCreds ?? (await fetchVmCredentialsSoft(deps, workspaceId, vmId, wsRegion));
|
|
849
|
+
const key = vm?.api_key ?? creds.apiKey;
|
|
850
|
+
const autoLogonUser = creds.adminUsername ?? "";
|
|
851
|
+
const autoLogonPass = creds.adminPassword ?? "";
|
|
466
852
|
// Use single quotes for values that may contain special chars (&, !, }, etc.)
|
|
467
853
|
const psEscape = (s) => s.replace(/'/g, "''"); // only ' needs escaping in single-quoted PS strings
|
|
468
854
|
const envVars = [
|
|
@@ -560,7 +946,7 @@ print(json.dumps({"clipboard": clipboard_text}))
|
|
|
560
946
|
region: regionParam,
|
|
561
947
|
}, async ({ workspaceId, vmId, region }) => {
|
|
562
948
|
try {
|
|
563
|
-
const wsRegion = region ?? getWorkspaceRegion(workspaceId);
|
|
949
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
564
950
|
const vmServiceUrl = deps.getVmServiceBase();
|
|
565
951
|
const authToken = deps.getAuthToken?.(wsRegion);
|
|
566
952
|
if (!authToken)
|
|
@@ -631,16 +1017,27 @@ print(json.dumps({"clipboard": clipboard_text}))
|
|
|
631
1017
|
// ── Start MDS after install ────────────────────────────────
|
|
632
1018
|
server.tool("start_mds_on_vm", "Start Minicor Desktop Service on an Azure VM via Azure Run Command. " +
|
|
633
1019
|
"Use after install_mds_on_vm when MDS installed but health check failed (Session 0 issue). " +
|
|
634
|
-
"Creates a one-shot scheduled task to launch MDS in the interactive desktop session."
|
|
1020
|
+
"Creates a one-shot scheduled task to launch MDS in the interactive desktop session. " +
|
|
1021
|
+
"STANDALONE VMs ONLY — refused when the VM's live /health.mode is broker (use the mode-aware restart there).", {
|
|
635
1022
|
workspaceId: z.number().describe("Workspace ID"),
|
|
636
1023
|
vmId: z.number().describe("VM ID from vm_list"),
|
|
637
1024
|
region: regionParam,
|
|
638
1025
|
}, async ({ workspaceId, vmId, region }) => {
|
|
639
|
-
const
|
|
640
|
-
if (
|
|
641
|
-
return
|
|
1026
|
+
const disabled = requireDirect();
|
|
1027
|
+
if (disabled)
|
|
1028
|
+
return disabled;
|
|
1029
|
+
const wsRegion = region ?? state.getWorkspaceRegion(workspaceId);
|
|
1030
|
+
const vm = state.getCachedVm(vmId);
|
|
1031
|
+
// Resolved up front so the mode probe below still has a tunnel URL.
|
|
1032
|
+
const coldCreds = vm
|
|
1033
|
+
? undefined
|
|
1034
|
+
: await fetchVmCredentialsSoft(deps, workspaceId, vmId, wsRegion);
|
|
1035
|
+
if (coldCreds && !coldCreds.tunnelUrl && !coldCreds.apiKey) {
|
|
1036
|
+
return text(`VM ${vmId} not found: it is not in the vm_list cache, and the VM service could not resolve it in workspace ${workspaceId}.`);
|
|
642
1037
|
}
|
|
643
|
-
const
|
|
1038
|
+
const refusal = await refuseLegacyStandaloneAction(vm?.tunnel_url ?? coldCreds?.tunnelUrl, "start_mds_on_vm", "it force-kills the running MDS process and relaunches the standalone entry point, which would take down the broker and its agents mid-flight.");
|
|
1039
|
+
if (refusal)
|
|
1040
|
+
return refusal;
|
|
644
1041
|
// PowerShell script that starts MDS in the interactive session
|
|
645
1042
|
// Uses WMI to find the logged-on user (more reliable than `query session`)
|
|
646
1043
|
const startScript = `
|