@solarisdk/mcp 0.4.5 → 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/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 +144 -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 -115191
- package/dist/solari-mcp.bundle.cjs +0 -113675
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,12 +45,48 @@ 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.
|
|
@@ -42,7 +96,7 @@ export function makeToolset(client, reg) {
|
|
|
42
96
|
// touch). The connect below just works, a few hundred milliseconds slower.
|
|
43
97
|
// Do not add an unpark call -- it would be a redundant round trip.
|
|
44
98
|
const live = async (id) => {
|
|
45
|
-
const e = need(id);
|
|
99
|
+
const e = await need(id);
|
|
46
100
|
if (e.handle.connected)
|
|
47
101
|
return e;
|
|
48
102
|
try {
|
|
@@ -65,12 +119,52 @@ export function makeToolset(client, reg) {
|
|
|
65
119
|
return {
|
|
66
120
|
solari_sandbox_create: {
|
|
67
121
|
description: "Create a headless sandbox (microVM). Returns its sessionId.",
|
|
68
|
-
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
|
+
},
|
|
69
159
|
handler: async (a) => {
|
|
160
|
+
const lifecycle = buildLifecycle(a);
|
|
70
161
|
const sbx = await client.sandboxes.create({
|
|
71
162
|
...(a.template ? { template: a.template } : {}),
|
|
72
163
|
...(a.cpu ? { cpu: a.cpu } : {}),
|
|
73
164
|
...(a.memMb ? { memMb: a.memMb } : {}),
|
|
165
|
+
...(a.ttlSeconds ? { ttlSeconds: a.ttlSeconds } : {}),
|
|
166
|
+
...(a.isolation ? { isolation: a.isolation } : {}),
|
|
167
|
+
...(lifecycle ? { lifecycle: lifecycle } : {}),
|
|
74
168
|
});
|
|
75
169
|
reg.sessions.set(sbx.sandboxId, { kind: "sandbox", handle: sbx, commands: [] });
|
|
76
170
|
return text({ sessionId: sbx.sandboxId });
|
|
@@ -78,11 +172,46 @@ export function makeToolset(client, reg) {
|
|
|
78
172
|
},
|
|
79
173
|
solari_desktop_create: {
|
|
80
174
|
description: "Create a GUI desktop (microVM). Returns sessionId + streamUrl (noVNC).",
|
|
81
|
-
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
|
+
},
|
|
82
207
|
handler: async (a) => {
|
|
208
|
+
const lifecycle = buildLifecycle(a);
|
|
83
209
|
const d = await client.desktops.create({
|
|
84
210
|
...(a.template ? { template: a.template } : {}),
|
|
85
211
|
...(a.resolution ? { resolution: a.resolution } : {}),
|
|
212
|
+
...(a.ttlSeconds ? { ttlSeconds: a.ttlSeconds } : {}),
|
|
213
|
+
...(a.isolation ? { isolation: a.isolation } : {}),
|
|
214
|
+
...(lifecycle ? { lifecycle: lifecycle } : {}),
|
|
86
215
|
});
|
|
87
216
|
reg.sessions.set(d.sessionId, { kind: "desktop", handle: d, commands: [] });
|
|
88
217
|
return text({ sessionId: d.sessionId, streamUrl: d.streamUrl });
|
|
@@ -147,7 +276,7 @@ export function makeToolset(client, reg) {
|
|
|
147
276
|
description: "Destroy a session by id.",
|
|
148
277
|
inputSchema: { sessionId: z.string() },
|
|
149
278
|
handler: async (a) => {
|
|
150
|
-
const e = need(a.sessionId);
|
|
279
|
+
const e = await need(a.sessionId);
|
|
151
280
|
await e.handle.kill();
|
|
152
281
|
reg.sessions.delete(a.sessionId);
|
|
153
282
|
return text({ ok: true });
|
|
@@ -392,7 +521,7 @@ export async function closeAllVmSessions(reg) {
|
|
|
392
521
|
}
|
|
393
522
|
}));
|
|
394
523
|
}
|
|
395
|
-
export function buildServerParts(client, browserCfg) {
|
|
524
|
+
export function buildServerParts(client, browserCfg, regs) {
|
|
396
525
|
const apiKey = browserCfg?.apiKey ?? process.env.SOLARI_API_KEY ?? "";
|
|
397
526
|
const c = client ??
|
|
398
527
|
new SolariClient({
|
|
@@ -410,8 +539,12 @@ export function buildServerParts(client, browserCfg) {
|
|
|
410
539
|
"https://api.getsolari.com",
|
|
411
540
|
};
|
|
412
541
|
const server = new McpServer({ name: "solari-mcp", version: VERSION });
|
|
413
|
-
|
|
414
|
-
|
|
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() };
|
|
415
548
|
registerToolset(server, {
|
|
416
549
|
...makeToolset(c, vmReg),
|
|
417
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": {
|