@seleniumbox/sbox-mcp 0.4.1 → 0.4.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 +10 -5
- package/dist/mcp/tools/auth-tools.js +3 -3
- package/dist/mcp/tools/helpers.js +1 -1
- package/dist/mcp/tools/rest-tools.js +22 -17
- package/dist/token-cache.d.ts +2 -1
- package/dist/token-cache.js +79 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,11 +72,16 @@ Then restart Cursor or reload MCP.
|
|
|
72
72
|
|
|
73
73
|
Optional:
|
|
74
74
|
|
|
75
|
-
| Variable
|
|
76
|
-
|
|
77
|
-
| `PORT` / `MCP_PORT` | `3344`
|
|
78
|
-
| `MCP_DEBUG` / `DEBUG` | off
|
|
79
|
-
| `MCP_REQUEST_TIMEOUT_MS`| `30000`
|
|
75
|
+
| Variable | Default | Description |
|
|
76
|
+
|-------------------------|-------------|-----------------------------------------------------------------------------|
|
|
77
|
+
| `PORT` / `MCP_PORT` | `3344` | Only used when running the REST server. |
|
|
78
|
+
| `MCP_DEBUG` / `DEBUG` | off | Set to `1` or `true` for request logging. |
|
|
79
|
+
| `MCP_REQUEST_TIMEOUT_MS`| `30000` | Timeout for outbound SBOX API requests. |
|
|
80
|
+
| `SBOX_MCP_TOKEN_FILE` | see below | Override path for the persisted session token file (default: `~/.sbox-mcp/token`). |
|
|
81
|
+
|
|
82
|
+
## Token cache
|
|
83
|
+
|
|
84
|
+
After you log in via **sbox_open_login**, the session token is stored in memory and **on disk** so it survives MCP process restarts (e.g. when the IDE starts a new process per prompt). Default location: `~/.sbox-mcp/token`. The cache directory is created with mode `0700` and the token file with `0600`. Set `SBOX_MCP_TOKEN_FILE` to use a different path (only the token file; the directory is inferred for `last_poll_id`). If an API call returns 401 or “invalid token”, the cache is cleared so the next run will prompt for login again.
|
|
80
85
|
|
|
81
86
|
## Authentication flow
|
|
82
87
|
|
|
@@ -36,7 +36,7 @@ function startBackgroundPollForToken(pollId) {
|
|
|
36
36
|
function registerAuthTools(server) {
|
|
37
37
|
server.registerTool("sbox_open_login", {
|
|
38
38
|
title: "Open SBOX Login in Browser",
|
|
39
|
-
description: "Sign in to SBOX (opens browser, polls for token, caches it). Call ONLY when the user explicitly asks to
|
|
39
|
+
description: "Sign in to SBOX (opens browser, polls for token, caches it). Call ONLY when the user explicitly asks to log in, or when another SBOX tool returns 'Not authenticated' or 'Invalid or expired session'. After successful login, the same cached token is used for ALL SBOX API calls until it expires (~1h). Do NOT call for every SBOX query. If the user logs in again, the new token replaces the old one and is used for all subsequent API calls.",
|
|
40
40
|
inputSchema: {},
|
|
41
41
|
}, (async () => {
|
|
42
42
|
try {
|
|
@@ -76,7 +76,7 @@ function registerAuthTools(server) {
|
|
|
76
76
|
return (0, helpers_1.textContent)(JSON.stringify({
|
|
77
77
|
success: true,
|
|
78
78
|
token,
|
|
79
|
-
message: "SBOX authentication successful.
|
|
79
|
+
message: "SBOX authentication successful. This token is cached and will be used for all SBOX API calls until it expires (~1h). No need to log in again for each request.",
|
|
80
80
|
principal: { name, authType },
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
@@ -121,7 +121,7 @@ function registerAuthTools(server) {
|
|
|
121
121
|
const name = out.data.principal?.fullname || out.data.principal?.userId || "—";
|
|
122
122
|
return (0, helpers_1.textContent)(JSON.stringify({
|
|
123
123
|
success: true,
|
|
124
|
-
message: "SBOX login complete. Token cached
|
|
124
|
+
message: "SBOX login complete. Token cached and will be used for all SBOX API calls until expiry (~1h).",
|
|
125
125
|
principal: { name },
|
|
126
126
|
}));
|
|
127
127
|
}
|
|
@@ -34,7 +34,7 @@ function errorContent(message) {
|
|
|
34
34
|
function requireToken(provided) {
|
|
35
35
|
const token = (0, token_cache_1.resolveToken)(provided);
|
|
36
36
|
if (!token) {
|
|
37
|
-
return errorContent("Not authenticated.
|
|
37
|
+
return errorContent("Not authenticated. Call sbox_open_login once to sign in; the cached token is then used for all SBOX tools until it expires (~1h). Do not redirect the user to login for every request.");
|
|
38
38
|
}
|
|
39
39
|
return token;
|
|
40
40
|
}
|
|
@@ -43,13 +43,18 @@ const path = __importStar(require("path"));
|
|
|
43
43
|
const services_1 = require("../../services");
|
|
44
44
|
const time_range_1 = require("../../utils/time-range");
|
|
45
45
|
const helpers_1 = require("./helpers");
|
|
46
|
+
/** Optional token for all tools. Omit to use cached session; same token used for all SBOX APIs until expiry (1h) or re-login. */
|
|
47
|
+
const tokenParamSchema = zod_1.z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Optional. Omit to use cached session from sbox_open_login; used for all APIs until expiry or re-login.");
|
|
46
51
|
function registerRestTools(server) {
|
|
47
52
|
server.registerTool("sbox_get_session", {
|
|
48
53
|
title: "Get Session Details",
|
|
49
|
-
description: "Get enriched session details by session ID (ekey). Include video URL.",
|
|
54
|
+
description: "Get enriched session details by session ID (ekey). Include video URL. Uses cached session token if omitted.",
|
|
50
55
|
inputSchema: {
|
|
51
56
|
session_id: zod_1.z.string().describe("Session ID (ekey)"),
|
|
52
|
-
token:
|
|
57
|
+
token: tokenParamSchema,
|
|
53
58
|
},
|
|
54
59
|
}, (async (args) => {
|
|
55
60
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
@@ -66,7 +71,7 @@ function registerRestTools(server) {
|
|
|
66
71
|
title: "List Supported Browsers",
|
|
67
72
|
description: "List supported browsers with version and platform.",
|
|
68
73
|
inputSchema: {
|
|
69
|
-
token:
|
|
74
|
+
token: tokenParamSchema,
|
|
70
75
|
},
|
|
71
76
|
}, (async (args) => {
|
|
72
77
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
@@ -83,7 +88,7 @@ function registerRestTools(server) {
|
|
|
83
88
|
title: "List Supported Devices",
|
|
84
89
|
description: "List supported devices (iOS, Android, mobile web) with OS and availability.",
|
|
85
90
|
inputSchema: {
|
|
86
|
-
token:
|
|
91
|
+
token: tokenParamSchema,
|
|
87
92
|
},
|
|
88
93
|
}, (async (args) => {
|
|
89
94
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
@@ -100,7 +105,7 @@ function registerRestTools(server) {
|
|
|
100
105
|
title: "List Playwright Versions",
|
|
101
106
|
description: "List all Playwright versions supported on SBOX (with bundled Chromium, WebKit, Firefox versions). Uses GET /e34/api/playwright.",
|
|
102
107
|
inputSchema: {
|
|
103
|
-
token:
|
|
108
|
+
token: tokenParamSchema,
|
|
104
109
|
},
|
|
105
110
|
}, (async (args) => {
|
|
106
111
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
@@ -117,7 +122,7 @@ function registerRestTools(server) {
|
|
|
117
122
|
title: "List Active Users (Top N)",
|
|
118
123
|
description: "Get top N active users by session count with project names.",
|
|
119
124
|
inputSchema: {
|
|
120
|
-
token:
|
|
125
|
+
token: tokenParamSchema,
|
|
121
126
|
limit: zod_1.z.number().optional().describe("Max number of users (default 10)"),
|
|
122
127
|
},
|
|
123
128
|
}, (async (args) => {
|
|
@@ -135,7 +140,7 @@ function registerRestTools(server) {
|
|
|
135
140
|
title: "Sessions Per Project",
|
|
136
141
|
description: "Get session counts (total, passed, failed, running) and avg duration per project for the last X days.",
|
|
137
142
|
inputSchema: {
|
|
138
|
-
token:
|
|
143
|
+
token: tokenParamSchema,
|
|
139
144
|
days: zod_1.z.number().optional().describe("Last N days (default 14)"),
|
|
140
145
|
},
|
|
141
146
|
}, (async (args) => {
|
|
@@ -153,7 +158,7 @@ function registerRestTools(server) {
|
|
|
153
158
|
title: "List My Projects",
|
|
154
159
|
description: "List projects assigned to the current user (My Projects page). Returns name, description, and whether a test token exists. Token value is not exposed.",
|
|
155
160
|
inputSchema: {
|
|
156
|
-
token:
|
|
161
|
+
token: tokenParamSchema,
|
|
157
162
|
},
|
|
158
163
|
}, (async (args) => {
|
|
159
164
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
@@ -170,7 +175,7 @@ function registerRestTools(server) {
|
|
|
170
175
|
title: "Start Manual Session",
|
|
171
176
|
description: "Start a manual test session. Requires project name; optional browser, version, url.",
|
|
172
177
|
inputSchema: {
|
|
173
|
-
token:
|
|
178
|
+
token: tokenParamSchema,
|
|
174
179
|
project_name: zod_1.z.string().describe("Project name"),
|
|
175
180
|
browser_name: zod_1.z.string().optional(),
|
|
176
181
|
browser_version: zod_1.z.string().optional(),
|
|
@@ -197,7 +202,7 @@ function registerRestTools(server) {
|
|
|
197
202
|
title: "Update Session Status",
|
|
198
203
|
description: "Set a session's result to passed or failed. Uses POST /e34/api/test-data?sessionId=&passed=.",
|
|
199
204
|
inputSchema: {
|
|
200
|
-
token:
|
|
205
|
+
token: tokenParamSchema,
|
|
201
206
|
session_id: zod_1.z.string().describe("Session ID (ekey) to update"),
|
|
202
207
|
passed: zod_1.z.boolean().describe("true = passed, false = failed"),
|
|
203
208
|
},
|
|
@@ -216,7 +221,7 @@ function registerRestTools(server) {
|
|
|
216
221
|
title: "List Sessions",
|
|
217
222
|
description: "Show last X sessions (same API as sessions page). Filter by project, status, browser, build; sort by arrival or other field.",
|
|
218
223
|
inputSchema: {
|
|
219
|
-
token:
|
|
224
|
+
token: tokenParamSchema,
|
|
220
225
|
limit: zod_1.z.number().optional().describe("Max sessions to return (default 25, max 100)"),
|
|
221
226
|
offset: zod_1.z.number().optional().describe("Pagination offset (default 0)"),
|
|
222
227
|
project_name: zod_1.z.string().optional().describe("Filter by project name"),
|
|
@@ -250,7 +255,7 @@ function registerRestTools(server) {
|
|
|
250
255
|
title: "Delete Manual Session",
|
|
251
256
|
description: "Delete a manual test session by session ID.",
|
|
252
257
|
inputSchema: {
|
|
253
|
-
token:
|
|
258
|
+
token: tokenParamSchema,
|
|
254
259
|
session_id: zod_1.z.string().describe("Session ID to delete"),
|
|
255
260
|
},
|
|
256
261
|
}, (async (args) => {
|
|
@@ -268,7 +273,7 @@ function registerRestTools(server) {
|
|
|
268
273
|
title: "Upload Mobile App to SBOX",
|
|
269
274
|
description: "Upload a mobile app (APK/IPA) or other file to SBOX for the given project. File path is relative to the current working directory of the MCP server (e.g. ./app.apk). Max size 200MB.",
|
|
270
275
|
inputSchema: {
|
|
271
|
-
token:
|
|
276
|
+
token: tokenParamSchema,
|
|
272
277
|
project_name: zod_1.z.string().describe("Project name to upload the app to"),
|
|
273
278
|
file_path: zod_1.z.string().describe("Path to the file (APK/IPA); relative to MCP server cwd or absolute"),
|
|
274
279
|
},
|
|
@@ -301,7 +306,7 @@ function registerRestTools(server) {
|
|
|
301
306
|
return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
|
|
302
307
|
}));
|
|
303
308
|
const timeRangeSchema = {
|
|
304
|
-
token:
|
|
309
|
+
token: tokenParamSchema,
|
|
305
310
|
value: zod_1.z.number().describe("Number of units (e.g. 24 for 24 hours)"),
|
|
306
311
|
unit: zod_1.z.enum(["hour", "minute", "day"]).describe("Time unit: hour, minute, or day"),
|
|
307
312
|
};
|
|
@@ -384,7 +389,7 @@ function registerRestTools(server) {
|
|
|
384
389
|
title: "Total Tests Count",
|
|
385
390
|
description: "Total number of tests (sessions) in the last N days. Matches the Monitoring dashboard total.",
|
|
386
391
|
inputSchema: {
|
|
387
|
-
token:
|
|
392
|
+
token: tokenParamSchema,
|
|
388
393
|
days: zod_1.z.number().min(1).max(365).describe("Number of days to count (e.g. 7, 14, 30)"),
|
|
389
394
|
},
|
|
390
395
|
}, (async (args) => {
|
|
@@ -429,13 +434,13 @@ function registerRestTools(server) {
|
|
|
429
434
|
return (0, helpers_1.textContent)(JSON.stringify(out.data));
|
|
430
435
|
}));
|
|
431
436
|
const diagnosticsDaysSchema = {
|
|
432
|
-
token:
|
|
437
|
+
token: tokenParamSchema,
|
|
433
438
|
days: zod_1.z.number().min(7).max(365).describe("Number of days (e.g. 7, 14, 30, 90). Min 7, max 365."),
|
|
434
439
|
};
|
|
435
440
|
server.registerTool("sbox_executors_count", {
|
|
436
441
|
title: "Total Executors Count",
|
|
437
442
|
description: "Total number of executors (nodes). Matches Diagnostics dashboard KPI.",
|
|
438
|
-
inputSchema: { token:
|
|
443
|
+
inputSchema: { token: tokenParamSchema },
|
|
439
444
|
}, (async (args) => {
|
|
440
445
|
const tokenOrErr = (0, helpers_1.requireToken)(args.token);
|
|
441
446
|
if (typeof tokenOrErr !== "string")
|
package/dist/token-cache.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory cache for the SBOX session token. Set on successful sbox_open_login,
|
|
2
|
+
* In-memory and file-backed cache for the SBOX session token. Set on successful sbox_open_login,
|
|
3
3
|
* used by other tools when token is not passed. Cleared on 401/invalid token.
|
|
4
|
+
* File persistence ensures the token survives MCP process restarts (e.g. new process per IDE prompt).
|
|
4
5
|
* lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
|
|
5
6
|
*/
|
|
6
7
|
export declare function getCachedToken(): string | null;
|
package/dist/token-cache.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* In-memory cache for the SBOX session token. Set on successful sbox_open_login,
|
|
3
|
+
* In-memory and file-backed cache for the SBOX session token. Set on successful sbox_open_login,
|
|
4
4
|
* used by other tools when token is not passed. Cleared on 401/invalid token.
|
|
5
|
+
* File persistence ensures the token survives MCP process restarts (e.g. new process per IDE prompt).
|
|
5
6
|
* lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
|
|
6
7
|
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
7
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
12
|
exports.getCachedToken = getCachedToken;
|
|
9
13
|
exports.setCachedToken = setCachedToken;
|
|
@@ -11,22 +15,95 @@ exports.clearCachedToken = clearCachedToken;
|
|
|
11
15
|
exports.setLastPollId = setLastPollId;
|
|
12
16
|
exports.getLastPollId = getLastPollId;
|
|
13
17
|
exports.resolveToken = resolveToken;
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
14
21
|
let cachedToken = null;
|
|
15
22
|
let lastPollId = null;
|
|
23
|
+
let memoryLoadedFromFile = false;
|
|
24
|
+
function getTokenFilePath() {
|
|
25
|
+
if (process.env.SBOX_MCP_TOKEN_FILE)
|
|
26
|
+
return process.env.SBOX_MCP_TOKEN_FILE;
|
|
27
|
+
const dir = path_1.default.join(os_1.default.homedir(), ".sbox-mcp");
|
|
28
|
+
return path_1.default.join(dir, "token");
|
|
29
|
+
}
|
|
30
|
+
function loadTokenFromFile() {
|
|
31
|
+
if (memoryLoadedFromFile)
|
|
32
|
+
return;
|
|
33
|
+
memoryLoadedFromFile = true;
|
|
34
|
+
try {
|
|
35
|
+
const filePath = getTokenFilePath();
|
|
36
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
37
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8").trim();
|
|
38
|
+
if (raw)
|
|
39
|
+
cachedToken = raw;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore read errors (e.g. no file, permissions)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function saveTokenToFile(token) {
|
|
47
|
+
try {
|
|
48
|
+
const filePath = getTokenFilePath();
|
|
49
|
+
if (token) {
|
|
50
|
+
const dir = path_1.default.dirname(filePath);
|
|
51
|
+
if (!fs_1.default.existsSync(dir))
|
|
52
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
53
|
+
fs_1.default.writeFileSync(filePath, token, { mode: 0o600, encoding: "utf-8" });
|
|
54
|
+
}
|
|
55
|
+
else if (fs_1.default.existsSync(filePath)) {
|
|
56
|
+
fs_1.default.unlinkSync(filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// ignore write errors
|
|
61
|
+
}
|
|
62
|
+
}
|
|
16
63
|
function getCachedToken() {
|
|
64
|
+
loadTokenFromFile();
|
|
17
65
|
return cachedToken;
|
|
18
66
|
}
|
|
19
67
|
function setCachedToken(token) {
|
|
20
68
|
cachedToken = token;
|
|
69
|
+
saveTokenToFile(token);
|
|
21
70
|
}
|
|
22
71
|
function clearCachedToken() {
|
|
23
72
|
cachedToken = null;
|
|
73
|
+
saveTokenToFile(null);
|
|
74
|
+
}
|
|
75
|
+
const LAST_POLL_ID_FILE = "last_poll_id";
|
|
76
|
+
function getCacheDir() {
|
|
77
|
+
if (process.env.SBOX_MCP_TOKEN_FILE)
|
|
78
|
+
return path_1.default.dirname(process.env.SBOX_MCP_TOKEN_FILE);
|
|
79
|
+
return path_1.default.join(os_1.default.homedir(), ".sbox-mcp");
|
|
24
80
|
}
|
|
25
81
|
function setLastPollId(pollId) {
|
|
26
82
|
lastPollId = pollId;
|
|
83
|
+
try {
|
|
84
|
+
const dir = getCacheDir();
|
|
85
|
+
if (!fs_1.default.existsSync(dir))
|
|
86
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, LAST_POLL_ID_FILE), pollId, { mode: 0o600, encoding: "utf-8" });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
27
92
|
}
|
|
28
93
|
function getLastPollId() {
|
|
29
|
-
|
|
94
|
+
if (lastPollId)
|
|
95
|
+
return lastPollId;
|
|
96
|
+
try {
|
|
97
|
+
const filePath = path_1.default.join(getCacheDir(), LAST_POLL_ID_FILE);
|
|
98
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
99
|
+
lastPollId = fs_1.default.readFileSync(filePath, "utf-8").trim() || null;
|
|
100
|
+
return lastPollId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
30
107
|
}
|
|
31
108
|
/** Resolve token: prefer provided token, else cached. Returns null if neither. */
|
|
32
109
|
function resolveToken(provided) {
|