@solarisdk/mcp 0.4.4 → 0.4.6
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 +1 -0
- package/dist/browser-tools/auth.js +3 -3
- package/dist/browser-tools/capture.js +2 -2
- package/dist/browser-tools/context.d.ts +3 -1
- package/dist/browser-tools/interaction.js +4 -4
- package/dist/browser-tools/navigation.js +1 -1
- package/dist/browser.js +77 -4
- package/dist/http.js +150 -75
- package/dist/server.d.ts +4 -1
- package/dist/server.js +205 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/solari-mcp-http.bundle.cjs +0 -115134
- package/dist/solari-mcp.bundle.cjs +0 -113618
package/dist/server.js
CHANGED
|
@@ -20,6 +20,24 @@ import { makeBrowserToolset, releaseAllBrowserSessions, } from "./browser.js";
|
|
|
20
20
|
const MAX_TOOL_TEXT = 30_000;
|
|
21
21
|
// Pages of `GET /sandboxes` solari_list will follow per kind (100 rows each).
|
|
22
22
|
const MAX_LIST_PAGES = 5;
|
|
23
|
+
// The four park/checkpoint tool args, flat on the wire-facing tool schemas
|
|
24
|
+
// (see the create tools below), assembled into the nested `lifecycle` shape
|
|
25
|
+
// the gateway actually accepts. Returns undefined -- not an empty object --
|
|
26
|
+
// when the caller set none of them, so a create that never mentions lifecycle
|
|
27
|
+
// does not send `lifecycle: {}` (the gateway defaults onTimeout/autoResume
|
|
28
|
+
// itself; sending an empty object would just be noise on the wire).
|
|
29
|
+
function buildLifecycle(a) {
|
|
30
|
+
const lc = {};
|
|
31
|
+
if (a.parkAfterMs)
|
|
32
|
+
lc.parkAfterMs = a.parkAfterMs;
|
|
33
|
+
if (a.parkMaxMs)
|
|
34
|
+
lc.parkMaxMs = a.parkMaxMs;
|
|
35
|
+
if (a.hibernateAfterParkedMs)
|
|
36
|
+
lc.hibernateAfterParkedMs = a.hibernateAfterParkedMs;
|
|
37
|
+
if (a.checkpointEveryMs)
|
|
38
|
+
lc.checkpointEveryMs = a.checkpointEveryMs;
|
|
39
|
+
return Object.keys(lc).length > 0 ? lc : undefined;
|
|
40
|
+
}
|
|
23
41
|
const text = (o) => {
|
|
24
42
|
const s = typeof o === "string" ? o : JSON.stringify(o, null, 2);
|
|
25
43
|
const capped = s.length > MAX_TOOL_TEXT
|
|
@@ -27,17 +45,58 @@ const text = (o) => {
|
|
|
27
45
|
: s;
|
|
28
46
|
return { content: [{ type: "text", text: capped }] };
|
|
29
47
|
};
|
|
48
|
+
// Rebuild a registry Entry for `id` by asking the gateway what it is and
|
|
49
|
+
// re-attaching. This is the same sequence solari_connect performs by hand, and
|
|
50
|
+
// it is what makes the registry a CACHE rather than authoritative state: the
|
|
51
|
+
// gateway already knows every session, and the client already carries the id
|
|
52
|
+
// (create tools return it, every other tool takes it).
|
|
53
|
+
async function rehydrate(client, reg, id) {
|
|
54
|
+
let kind = reg.sessions.get(id)?.kind ?? "sandbox";
|
|
55
|
+
let state;
|
|
56
|
+
try {
|
|
57
|
+
const view = await client.sandboxes.get(id);
|
|
58
|
+
if (view?.kind === "desktop" || view?.kind === "sandbox")
|
|
59
|
+
kind = view.kind;
|
|
60
|
+
state = view?.state;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// The gateway is the authority on existence; if it cannot be reached we
|
|
64
|
+
// fall through to connect() and let ITS error surface, rather than
|
|
65
|
+
// inventing an "unknown sessionId" that hides a gateway outage.
|
|
66
|
+
}
|
|
67
|
+
const handle = kind === "desktop" ? await client.desktops.connect(id) : await client.sandboxes.connect(id);
|
|
68
|
+
// desktops.connect() auto-resumes; sandboxes.connect() does not.
|
|
69
|
+
if (kind === "sandbox" && state === "paused" && typeof handle.resume === "function") {
|
|
70
|
+
await handle.resume();
|
|
71
|
+
}
|
|
72
|
+
// commands: [] is correct, not a loss. The array only exists to keep the
|
|
73
|
+
// rejecting exit promise of a background command owned; no tool ever reads
|
|
74
|
+
// it back, so a rebuilt Entry owes nothing to the pod that started one.
|
|
75
|
+
const e = { kind, handle, commands: [] };
|
|
76
|
+
reg.sessions.set(id, e);
|
|
77
|
+
return e;
|
|
78
|
+
}
|
|
30
79
|
export function makeToolset(client, reg) {
|
|
31
|
-
|
|
80
|
+
// A MISS IS NOT AN ERROR — it is a cold cache. Before 2026-09-21 this threw
|
|
81
|
+
// `unknown sessionId`, which is why the hosted connector could only ever run
|
|
82
|
+
// one replica: a request that landed on a pod which had not served the
|
|
83
|
+
// create call was indistinguishable from a request for a session that never
|
|
84
|
+
// existed. Rebuilding on demand is what lets any pod serve any request.
|
|
85
|
+
const need = async (id) => {
|
|
32
86
|
const e = reg.sessions.get(id);
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
return
|
|
87
|
+
if (e)
|
|
88
|
+
return e;
|
|
89
|
+
return rehydrate(client, reg, id);
|
|
36
90
|
};
|
|
37
91
|
// A paused session refuses the /control upgrade with 409, so resume it and
|
|
38
92
|
// retry rather than surfacing an opaque websocket error.
|
|
93
|
+
//
|
|
94
|
+
// A PARKED session needs nothing here: park keeps the slot, the host and the
|
|
95
|
+
// routing, and the gateway unparks on the /control upgrade itself (wake-on-
|
|
96
|
+
// touch). The connect below just works, a few hundred milliseconds slower.
|
|
97
|
+
// Do not add an unpark call -- it would be a redundant round trip.
|
|
39
98
|
const live = async (id) => {
|
|
40
|
-
const e = need(id);
|
|
99
|
+
const e = await need(id);
|
|
41
100
|
if (e.handle.connected)
|
|
42
101
|
return e;
|
|
43
102
|
try {
|
|
@@ -60,12 +119,52 @@ export function makeToolset(client, reg) {
|
|
|
60
119
|
return {
|
|
61
120
|
solari_sandbox_create: {
|
|
62
121
|
description: "Create a headless sandbox (microVM). Returns its sessionId.",
|
|
63
|
-
inputSchema: {
|
|
122
|
+
inputSchema: {
|
|
123
|
+
template: z.string().optional(),
|
|
124
|
+
cpu: z.number().optional(),
|
|
125
|
+
memMb: z.number().optional(),
|
|
126
|
+
// Kept FLAT rather than a nested `lifecycle` object, matching every
|
|
127
|
+
// other tool schema in this file -- a flat schema is what an LLM
|
|
128
|
+
// tool-calling interface parses most reliably. Mapped into the
|
|
129
|
+
// gateway's nested `lifecycle` shape only inside the handler.
|
|
130
|
+
ttlSeconds: z
|
|
131
|
+
.number()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Legacy idle-window fallback in seconds, only used when the gateway's default " +
|
|
134
|
+
"timeout is not already set by another field. Usually unnecessary."),
|
|
135
|
+
isolation: z
|
|
136
|
+
.enum(["shared", "dedicated", "hardened"])
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("VM isolation tier. \"shared\" runs inside a VM shared with your other shared " +
|
|
139
|
+
"sandboxes (smaller footprint, faster start). \"dedicated\" (default) is your " +
|
|
140
|
+
"own VM. \"hardened\" is your own VM with outbound network blocked -- verified " +
|
|
141
|
+
"on staging, not yet on prod; refused if the assigned host doesn't support it."),
|
|
142
|
+
parkAfterMs: z
|
|
143
|
+
.number()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Auto-park (freeze, keep the slot) after this many ms of no activity."),
|
|
146
|
+
parkMaxMs: z
|
|
147
|
+
.number()
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Wake a parked session unconditionally after this many ms, even if idle."),
|
|
150
|
+
hibernateAfterParkedMs: z
|
|
151
|
+
.number()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Escalate a parked session to a full hibernate after this many ms parked."),
|
|
154
|
+
checkpointEveryMs: z
|
|
155
|
+
.number()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("Take a durable checkpoint at most this often while the session runs."),
|
|
158
|
+
},
|
|
64
159
|
handler: async (a) => {
|
|
160
|
+
const lifecycle = buildLifecycle(a);
|
|
65
161
|
const sbx = await client.sandboxes.create({
|
|
66
162
|
...(a.template ? { template: a.template } : {}),
|
|
67
163
|
...(a.cpu ? { cpu: a.cpu } : {}),
|
|
68
164
|
...(a.memMb ? { memMb: a.memMb } : {}),
|
|
165
|
+
...(a.ttlSeconds ? { ttlSeconds: a.ttlSeconds } : {}),
|
|
166
|
+
...(a.isolation ? { isolation: a.isolation } : {}),
|
|
167
|
+
...(lifecycle ? { lifecycle: lifecycle } : {}),
|
|
69
168
|
});
|
|
70
169
|
reg.sessions.set(sbx.sandboxId, { kind: "sandbox", handle: sbx, commands: [] });
|
|
71
170
|
return text({ sessionId: sbx.sandboxId });
|
|
@@ -73,11 +172,46 @@ export function makeToolset(client, reg) {
|
|
|
73
172
|
},
|
|
74
173
|
solari_desktop_create: {
|
|
75
174
|
description: "Create a GUI desktop (microVM). Returns sessionId + streamUrl (noVNC).",
|
|
76
|
-
inputSchema: {
|
|
175
|
+
inputSchema: {
|
|
176
|
+
template: z.string().optional(),
|
|
177
|
+
resolution: z.string().optional(),
|
|
178
|
+
ttlSeconds: z
|
|
179
|
+
.number()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Legacy idle-window fallback in seconds, only used when the gateway's default " +
|
|
182
|
+
"timeout is not already set by another field. Usually unnecessary."),
|
|
183
|
+
isolation: z
|
|
184
|
+
.enum(["dedicated", "hardened"])
|
|
185
|
+
.optional()
|
|
186
|
+
.describe("VM isolation tier. \"dedicated\" (default) is your own VM. \"hardened\" is your " +
|
|
187
|
+
"own VM with outbound network blocked -- verified on staging, not yet on prod; " +
|
|
188
|
+
"refused if the assigned host doesn't support it. \"shared\" is sandbox-only " +
|
|
189
|
+
"(a desktop's display is per-VM) and is not offered here."),
|
|
190
|
+
parkAfterMs: z
|
|
191
|
+
.number()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Auto-park (freeze, keep the slot) after this many ms of no activity."),
|
|
194
|
+
parkMaxMs: z
|
|
195
|
+
.number()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Wake a parked session unconditionally after this many ms, even if idle."),
|
|
198
|
+
hibernateAfterParkedMs: z
|
|
199
|
+
.number()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Escalate a parked session to a full hibernate after this many ms parked."),
|
|
202
|
+
checkpointEveryMs: z
|
|
203
|
+
.number()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Take a durable checkpoint at most this often while the session runs."),
|
|
206
|
+
},
|
|
77
207
|
handler: async (a) => {
|
|
208
|
+
const lifecycle = buildLifecycle(a);
|
|
78
209
|
const d = await client.desktops.create({
|
|
79
210
|
...(a.template ? { template: a.template } : {}),
|
|
80
211
|
...(a.resolution ? { resolution: a.resolution } : {}),
|
|
212
|
+
...(a.ttlSeconds ? { ttlSeconds: a.ttlSeconds } : {}),
|
|
213
|
+
...(a.isolation ? { isolation: a.isolation } : {}),
|
|
214
|
+
...(lifecycle ? { lifecycle: lifecycle } : {}),
|
|
81
215
|
});
|
|
82
216
|
reg.sessions.set(d.sessionId, { kind: "desktop", handle: d, commands: [] });
|
|
83
217
|
return text({ sessionId: d.sessionId, streamUrl: d.streamUrl });
|
|
@@ -142,7 +276,7 @@ export function makeToolset(client, reg) {
|
|
|
142
276
|
description: "Destroy a session by id.",
|
|
143
277
|
inputSchema: { sessionId: z.string() },
|
|
144
278
|
handler: async (a) => {
|
|
145
|
-
const e = need(a.sessionId);
|
|
279
|
+
const e = await need(a.sessionId);
|
|
146
280
|
await e.handle.kill();
|
|
147
281
|
reg.sessions.delete(a.sessionId);
|
|
148
282
|
return text({ ok: true });
|
|
@@ -275,6 +409,62 @@ export function makeToolset(client, reg) {
|
|
|
275
409
|
return text({ ok: true });
|
|
276
410
|
},
|
|
277
411
|
},
|
|
412
|
+
solari_mouse_down: {
|
|
413
|
+
description: "Press and HOLD a mouse button at (x, y) — the start of a drag (e.g. grab a window " +
|
|
414
|
+
"title bar). Follow with solari_mouse_move to drag while held, then solari_mouse_up " +
|
|
415
|
+
"to release. For a simple one-shot drag prefer solari_drag.",
|
|
416
|
+
inputSchema: {
|
|
417
|
+
sessionId: z.string(),
|
|
418
|
+
x: z.number(),
|
|
419
|
+
y: z.number(),
|
|
420
|
+
button: z.enum(["left", "right", "middle"]).optional(),
|
|
421
|
+
},
|
|
422
|
+
handler: async (a) => {
|
|
423
|
+
const e = await desktop(a.sessionId);
|
|
424
|
+
await e.handle.mouse.down(a.x, a.y, a.button ?? "left");
|
|
425
|
+
return text({ ok: true });
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
solari_mouse_move: {
|
|
429
|
+
description: "Move the pointer to (x, y). While a button is held (solari_mouse_down) this drags.",
|
|
430
|
+
inputSchema: { sessionId: z.string(), x: z.number(), y: z.number() },
|
|
431
|
+
handler: async (a) => {
|
|
432
|
+
const e = await desktop(a.sessionId);
|
|
433
|
+
await e.handle.mouse.move(a.x, a.y);
|
|
434
|
+
return text({ ok: true });
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
solari_mouse_up: {
|
|
438
|
+
description: "Release the held mouse button at (x, y) — the end of a drag started with solari_mouse_down.",
|
|
439
|
+
inputSchema: {
|
|
440
|
+
sessionId: z.string(),
|
|
441
|
+
x: z.number(),
|
|
442
|
+
y: z.number(),
|
|
443
|
+
button: z.enum(["left", "right", "middle"]).optional(),
|
|
444
|
+
},
|
|
445
|
+
handler: async (a) => {
|
|
446
|
+
const e = await desktop(a.sessionId);
|
|
447
|
+
await e.handle.mouse.up(a.x, a.y, a.button ?? "left");
|
|
448
|
+
return text({ ok: true });
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
solari_drag: {
|
|
452
|
+
description: "Drag in one call: press at (fromX, fromY), move to (toX, toY), release. " +
|
|
453
|
+
"Good for moving windows, sliders, and drag-and-drop.",
|
|
454
|
+
inputSchema: {
|
|
455
|
+
sessionId: z.string(),
|
|
456
|
+
fromX: z.number(),
|
|
457
|
+
fromY: z.number(),
|
|
458
|
+
toX: z.number(),
|
|
459
|
+
toY: z.number(),
|
|
460
|
+
button: z.enum(["left", "right", "middle"]).optional(),
|
|
461
|
+
},
|
|
462
|
+
handler: async (a) => {
|
|
463
|
+
const e = await desktop(a.sessionId);
|
|
464
|
+
await e.handle.mouse.drag({ x: a.fromX, y: a.fromY }, { x: a.toX, y: a.toY }, a.button ?? "left");
|
|
465
|
+
return text({ ok: true });
|
|
466
|
+
},
|
|
467
|
+
},
|
|
278
468
|
solari_type: {
|
|
279
469
|
description: "Type text into the focused desktop element.",
|
|
280
470
|
inputSchema: { sessionId: z.string(), text: z.string() },
|
|
@@ -331,7 +521,7 @@ export async function closeAllVmSessions(reg) {
|
|
|
331
521
|
}
|
|
332
522
|
}));
|
|
333
523
|
}
|
|
334
|
-
export function buildServerParts(client, browserCfg) {
|
|
524
|
+
export function buildServerParts(client, browserCfg, regs) {
|
|
335
525
|
const apiKey = browserCfg?.apiKey ?? process.env.SOLARI_API_KEY ?? "";
|
|
336
526
|
const c = client ??
|
|
337
527
|
new SolariClient({
|
|
@@ -349,8 +539,12 @@ export function buildServerParts(client, browserCfg) {
|
|
|
349
539
|
"https://api.getsolari.com",
|
|
350
540
|
};
|
|
351
541
|
const server = new McpServer({ name: "solari-mcp", version: VERSION });
|
|
352
|
-
|
|
353
|
-
|
|
542
|
+
// Registries may be SUPPLIED by the caller. The hosted HTTP transport is
|
|
543
|
+
// stateless (a fresh McpServer per request), so it keeps one pair of
|
|
544
|
+
// registries per API key and passes them in — otherwise every request would
|
|
545
|
+
// start with a cold cache and re-attach every handle from the gateway.
|
|
546
|
+
const browserReg = regs?.browserReg ?? { sessions: new Map() };
|
|
547
|
+
const vmReg = regs?.vmReg ?? { sessions: new Map() };
|
|
354
548
|
registerToolset(server, {
|
|
355
549
|
...makeToolset(c, vmReg),
|
|
356
550
|
...makeBrowserToolset(bCfg, browserReg),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.4.
|
|
1
|
+
export declare const VERSION = "0.4.6";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Model Context Protocol server for the Solari cloud browser, sandboxes + desktops \u2014 drive them from Claude Desktop/Cowork, Claude Code, Cursor, etc.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|