@seleniumbox/sbox-mcp 1.0.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.
Files changed (141) hide show
  1. package/PUBLISHING.md +115 -0
  2. package/README.md +79 -0
  3. package/dist/adapters/auth.adapter.d.ts +46 -0
  4. package/dist/adapters/auth.adapter.js +54 -0
  5. package/dist/adapters/browser.adapter.d.ts +43 -0
  6. package/dist/adapters/browser.adapter.js +39 -0
  7. package/dist/adapters/device.adapter.d.ts +60 -0
  8. package/dist/adapters/device.adapter.js +40 -0
  9. package/dist/adapters/diagnostics.adapter.d.ts +73 -0
  10. package/dist/adapters/diagnostics.adapter.js +89 -0
  11. package/dist/adapters/index.d.ts +16 -0
  12. package/dist/adapters/index.js +20 -0
  13. package/dist/adapters/project.adapter.d.ts +38 -0
  14. package/dist/adapters/project.adapter.js +39 -0
  15. package/dist/adapters/sbox-api.client.d.ts +31 -0
  16. package/dist/adapters/sbox-api.client.js +166 -0
  17. package/dist/adapters/session.adapter.d.ts +77 -0
  18. package/dist/adapters/session.adapter.js +95 -0
  19. package/dist/adapters/stats.adapter.d.ts +72 -0
  20. package/dist/adapters/stats.adapter.js +108 -0
  21. package/dist/adapters/upload.adapter.d.ts +16 -0
  22. package/dist/adapters/upload.adapter.js +25 -0
  23. package/dist/adapters/user.adapter.d.ts +24 -0
  24. package/dist/adapters/user.adapter.js +25 -0
  25. package/dist/app.d.ts +2 -0
  26. package/dist/app.js +16 -0
  27. package/dist/config/env.d.ts +23 -0
  28. package/dist/config/env.js +64 -0
  29. package/dist/config/index.d.ts +1 -0
  30. package/dist/config/index.js +8 -0
  31. package/dist/controllers/analytics.controller.d.ts +10 -0
  32. package/dist/controllers/analytics.controller.js +127 -0
  33. package/dist/controllers/auth-device.controller.d.ts +7 -0
  34. package/dist/controllers/auth-device.controller.js +60 -0
  35. package/dist/controllers/auth.controller.d.ts +5 -0
  36. package/dist/controllers/auth.controller.js +20 -0
  37. package/dist/controllers/browser.controller.d.ts +5 -0
  38. package/dist/controllers/browser.controller.js +23 -0
  39. package/dist/controllers/device.controller.d.ts +5 -0
  40. package/dist/controllers/device.controller.js +23 -0
  41. package/dist/controllers/index.d.ts +6 -0
  42. package/dist/controllers/index.js +28 -0
  43. package/dist/controllers/project-stats.controller.d.ts +5 -0
  44. package/dist/controllers/project-stats.controller.js +29 -0
  45. package/dist/controllers/session.controller.d.ts +9 -0
  46. package/dist/controllers/session.controller.js +120 -0
  47. package/dist/controllers/upload.controller.d.ts +5 -0
  48. package/dist/controllers/upload.controller.js +44 -0
  49. package/dist/controllers/user.controller.d.ts +5 -0
  50. package/dist/controllers/user.controller.js +29 -0
  51. package/dist/device-flow.store.d.ts +7 -0
  52. package/dist/device-flow.store.js +23 -0
  53. package/dist/dto/auth.dto.d.ts +26 -0
  54. package/dist/dto/auth.dto.js +9 -0
  55. package/dist/dto/browser.dto.d.ts +13 -0
  56. package/dist/dto/browser.dto.js +2 -0
  57. package/dist/dto/device.dto.d.ts +15 -0
  58. package/dist/dto/device.dto.js +2 -0
  59. package/dist/dto/index.d.ts +6 -0
  60. package/dist/dto/index.js +22 -0
  61. package/dist/dto/project-stats.dto.d.ts +8 -0
  62. package/dist/dto/project-stats.dto.js +2 -0
  63. package/dist/dto/session.dto.d.ts +50 -0
  64. package/dist/dto/session.dto.js +9 -0
  65. package/dist/dto/user.dto.d.ts +6 -0
  66. package/dist/dto/user.dto.js +2 -0
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.js +35 -0
  69. package/dist/mcp/tools/auth-tools.d.ts +5 -0
  70. package/dist/mcp/tools/auth-tools.js +132 -0
  71. package/dist/mcp/tools/helpers.d.ts +26 -0
  72. package/dist/mcp/tools/helpers.js +53 -0
  73. package/dist/mcp/tools/index.d.ts +5 -0
  74. package/dist/mcp/tools/index.js +12 -0
  75. package/dist/mcp/tools/rest-tools.d.ts +5 -0
  76. package/dist/mcp/tools/rest-tools.js +545 -0
  77. package/dist/mcp-server.d.ts +6 -0
  78. package/dist/mcp-server.js +28 -0
  79. package/dist/middleware/auth.middleware.d.ts +10 -0
  80. package/dist/middleware/auth.middleware.js +23 -0
  81. package/dist/middleware/debug-log.middleware.d.ts +6 -0
  82. package/dist/middleware/debug-log.middleware.js +84 -0
  83. package/dist/middleware/error.middleware.d.ts +8 -0
  84. package/dist/middleware/error.middleware.js +24 -0
  85. package/dist/middleware/validate.middleware.d.ts +7 -0
  86. package/dist/middleware/validate.middleware.js +42 -0
  87. package/dist/routes/analytics.routes.d.ts +1 -0
  88. package/dist/routes/analytics.routes.js +15 -0
  89. package/dist/routes/auth.routes.d.ts +1 -0
  90. package/dist/routes/auth.routes.js +15 -0
  91. package/dist/routes/browser.routes.d.ts +1 -0
  92. package/dist/routes/browser.routes.js +11 -0
  93. package/dist/routes/device.routes.d.ts +1 -0
  94. package/dist/routes/device.routes.js +11 -0
  95. package/dist/routes/index.d.ts +1 -0
  96. package/dist/routes/index.js +27 -0
  97. package/dist/routes/mcp.routes.d.ts +6 -0
  98. package/dist/routes/mcp.routes.js +70 -0
  99. package/dist/routes/project-stats.routes.d.ts +1 -0
  100. package/dist/routes/project-stats.routes.js +11 -0
  101. package/dist/routes/session.routes.d.ts +1 -0
  102. package/dist/routes/session.routes.js +23 -0
  103. package/dist/routes/upload.routes.d.ts +1 -0
  104. package/dist/routes/upload.routes.js +20 -0
  105. package/dist/routes/user.routes.d.ts +1 -0
  106. package/dist/routes/user.routes.js +11 -0
  107. package/dist/server.d.ts +1 -0
  108. package/dist/server.js +17 -0
  109. package/dist/services/analytics.service.d.ts +88 -0
  110. package/dist/services/analytics.service.js +98 -0
  111. package/dist/services/auth.service.d.ts +27 -0
  112. package/dist/services/auth.service.js +68 -0
  113. package/dist/services/browser.service.d.ts +25 -0
  114. package/dist/services/browser.service.js +43 -0
  115. package/dist/services/device.service.d.ts +18 -0
  116. package/dist/services/device.service.js +58 -0
  117. package/dist/services/diagnostics.service.d.ts +87 -0
  118. package/dist/services/diagnostics.service.js +92 -0
  119. package/dist/services/enrichment.service.d.ts +9 -0
  120. package/dist/services/enrichment.service.js +112 -0
  121. package/dist/services/index.d.ts +20 -0
  122. package/dist/services/index.js +31 -0
  123. package/dist/services/project.service.d.ts +22 -0
  124. package/dist/services/project.service.js +31 -0
  125. package/dist/services/session.service.d.ts +62 -0
  126. package/dist/services/session.service.js +104 -0
  127. package/dist/services/sessions-per-project.service.d.ts +18 -0
  128. package/dist/services/sessions-per-project.service.js +57 -0
  129. package/dist/services/upload.service.d.ts +20 -0
  130. package/dist/services/upload.service.js +29 -0
  131. package/dist/services/user.service.d.ts +17 -0
  132. package/dist/services/user.service.js +39 -0
  133. package/dist/token-cache.d.ts +13 -0
  134. package/dist/token-cache.js +114 -0
  135. package/dist/utils/logger.d.ts +11 -0
  136. package/dist/utils/logger.js +31 -0
  137. package/dist/utils/open-browser.d.ts +6 -0
  138. package/dist/utils/open-browser.js +23 -0
  139. package/dist/utils/time-range.d.ts +11 -0
  140. package/dist/utils/time-range.js +16 -0
  141. package/package.json +42 -0
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Shared helpers for MCP tool responses and token handling.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_UPLOAD_SIZE = void 0;
7
+ exports.textContent = textContent;
8
+ exports.friendlyError = friendlyError;
9
+ exports.errorContent = errorContent;
10
+ exports.requireToken = requireToken;
11
+ exports.isAuthError = isAuthError;
12
+ exports.clearTokenOnAuthError = clearTokenOnAuthError;
13
+ exports.sleep = sleep;
14
+ const token_cache_1 = require("../../token-cache");
15
+ exports.MAX_UPLOAD_SIZE = 200 * 1024 * 1024; // 200MB
16
+ function textContent(text) {
17
+ return { content: [{ type: "text", text }] };
18
+ }
19
+ function friendlyError(message) {
20
+ const m = (message || "").toLowerCase();
21
+ if (m.includes("unauthorized") || (m.includes("invalid") && m.includes("token")))
22
+ return "Invalid or expired session. Please log in again.";
23
+ if (m.includes("forbidden"))
24
+ return "You don't have permission for this action.";
25
+ if (m.includes("not found"))
26
+ return "The requested resource was not found.";
27
+ if (m.includes("timeout") || m.includes("econnrefused"))
28
+ return "The request timed out or the service is unavailable. Please try again.";
29
+ return message || "Something went wrong. Please try again.";
30
+ }
31
+ function errorContent(message) {
32
+ return { content: [{ type: "text", text: `Error: ${friendlyError(message)}` }] };
33
+ }
34
+ function requireToken(provided) {
35
+ const token = (0, token_cache_1.resolveToken)(provided);
36
+ if (!token) {
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
+ }
39
+ return token;
40
+ }
41
+ function isAuthError(message) {
42
+ const m = (message || "").toLowerCase();
43
+ return (m.includes("unauthorized") ||
44
+ (m.includes("invalid") && m.includes("token")) ||
45
+ m.includes("expired"));
46
+ }
47
+ function clearTokenOnAuthError(message) {
48
+ if (isAuthError(message))
49
+ (0, token_cache_1.clearCachedToken)();
50
+ }
51
+ function sleep(ms) {
52
+ return new Promise((resolve) => setTimeout(resolve, ms));
53
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MCP tools entrypoint. Composes auth and rest tool registration.
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
5
+ export declare function registerSboxTools(server: McpServer): void;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * MCP tools entrypoint. Composes auth and rest tool registration.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerSboxTools = registerSboxTools;
7
+ const auth_tools_1 = require("./auth-tools");
8
+ const rest_tools_1 = require("./rest-tools");
9
+ function registerSboxTools(server) {
10
+ (0, auth_tools_1.registerAuthTools)(server);
11
+ (0, rest_tools_1.registerRestTools)(server);
12
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MCP tools: session, browser, device, user, project, upload, analytics, diagnostics.
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
5
+ export declare function registerRestTools(server: McpServer): void;
@@ -0,0 +1,545 @@
1
+ "use strict";
2
+ /**
3
+ * MCP tools: session, browser, device, user, project, upload, analytics, diagnostics.
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerRestTools = registerRestTools;
40
+ const zod_1 = require("zod");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const services_1 = require("../../services");
44
+ const time_range_1 = require("../../utils/time-range");
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.");
51
+ function registerRestTools(server) {
52
+ server.registerTool("sbox_get_session", {
53
+ title: "Get Session Details",
54
+ description: "Get enriched session details by session ID (ekey). Include video URL. Uses cached session token if omitted.",
55
+ inputSchema: {
56
+ session_id: zod_1.z.string().describe("Session ID (ekey)"),
57
+ token: tokenParamSchema,
58
+ },
59
+ }, (async (args) => {
60
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
61
+ if (typeof tokenOrErr !== "string")
62
+ return tokenOrErr;
63
+ const out = await services_1.sessionService.getSessionDetail(args.session_id, tokenOrErr);
64
+ if (!out.success) {
65
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
66
+ return (0, helpers_1.errorContent)(out.message);
67
+ }
68
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
69
+ }));
70
+ server.registerTool("sbox_list_browsers", {
71
+ title: "List Supported Browsers",
72
+ description: "List supported browsers with version and platform.",
73
+ inputSchema: {
74
+ token: tokenParamSchema,
75
+ },
76
+ }, (async (args) => {
77
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
78
+ if (typeof tokenOrErr !== "string")
79
+ return tokenOrErr;
80
+ const out = await services_1.browserService.listSupportedBrowsers(tokenOrErr);
81
+ if (!out.success) {
82
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
83
+ return (0, helpers_1.errorContent)(out.message);
84
+ }
85
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
86
+ }));
87
+ server.registerTool("sbox_list_devices", {
88
+ title: "List Supported Devices",
89
+ description: "List supported devices (iOS, Android, mobile web) with OS and availability.",
90
+ inputSchema: {
91
+ token: tokenParamSchema,
92
+ },
93
+ }, (async (args) => {
94
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
95
+ if (typeof tokenOrErr !== "string")
96
+ return tokenOrErr;
97
+ const out = await services_1.deviceService.listSupportedDevices(tokenOrErr);
98
+ if (!out.success) {
99
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
100
+ return (0, helpers_1.errorContent)(out.message);
101
+ }
102
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
103
+ }));
104
+ server.registerTool("sbox_list_playwright_versions", {
105
+ title: "List Playwright Versions",
106
+ description: "List all Playwright versions supported on SBOX (with bundled Chromium, WebKit, Firefox versions). Uses GET /e34/api/playwright.",
107
+ inputSchema: {
108
+ token: tokenParamSchema,
109
+ },
110
+ }, (async (args) => {
111
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
112
+ if (typeof tokenOrErr !== "string")
113
+ return tokenOrErr;
114
+ const out = await services_1.browserService.listPlaywrightVersions(tokenOrErr);
115
+ if (!out.success) {
116
+ (0, helpers_1.clearTokenOnAuthError)(out.message ?? "");
117
+ return (0, helpers_1.errorContent)(out.message ?? "Playwright is not enabled");
118
+ }
119
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
120
+ }));
121
+ server.registerTool("sbox_list_active_users", {
122
+ title: "List Active Users (Top N)",
123
+ description: "Get top N active users by session count with project names.",
124
+ inputSchema: {
125
+ token: tokenParamSchema,
126
+ limit: zod_1.z.number().optional().describe("Max number of users (default 10)"),
127
+ },
128
+ }, (async (args) => {
129
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
130
+ if (typeof tokenOrErr !== "string")
131
+ return tokenOrErr;
132
+ const out = await services_1.userService.getActiveUsersTopN(tokenOrErr, args.limit ?? 10);
133
+ if (!out.success) {
134
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
135
+ return (0, helpers_1.errorContent)(out.message);
136
+ }
137
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
138
+ }));
139
+ server.registerTool("sbox_sessions_per_project", {
140
+ title: "Sessions Per Project",
141
+ description: "Get session counts (total, passed, failed, running) and avg duration per project for the last X days.",
142
+ inputSchema: {
143
+ token: tokenParamSchema,
144
+ days: zod_1.z.number().optional().describe("Last N days (default 14)"),
145
+ },
146
+ }, (async (args) => {
147
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
148
+ if (typeof tokenOrErr !== "string")
149
+ return tokenOrErr;
150
+ const out = await services_1.sessionsPerProjectService.getSessionsPerProject(tokenOrErr, args.days ?? 14);
151
+ if (!out.success) {
152
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
153
+ return (0, helpers_1.errorContent)(out.message);
154
+ }
155
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
156
+ }));
157
+ server.registerTool("sbox_list_my_projects", {
158
+ title: "List My Projects",
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.",
160
+ inputSchema: {
161
+ token: tokenParamSchema,
162
+ },
163
+ }, (async (args) => {
164
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
165
+ if (typeof tokenOrErr !== "string")
166
+ return tokenOrErr;
167
+ const out = await services_1.projectService.listMyProjects(tokenOrErr);
168
+ if (!out.success) {
169
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
170
+ return (0, helpers_1.errorContent)(out.message);
171
+ }
172
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
173
+ }));
174
+ server.registerTool("sbox_start_manual_session", {
175
+ title: "Start Manual Session",
176
+ description: "Start a manual test session. Requires project name; optional browser, version, url.",
177
+ inputSchema: {
178
+ token: tokenParamSchema,
179
+ project_name: zod_1.z.string().describe("Project name"),
180
+ browser_name: zod_1.z.string().optional(),
181
+ browser_version: zod_1.z.string().optional(),
182
+ url: zod_1.z.string().optional().describe("Initial URL to open"),
183
+ },
184
+ }, (async (args) => {
185
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
186
+ if (typeof tokenOrErr !== "string")
187
+ return tokenOrErr;
188
+ const body = {
189
+ projectName: args.project_name,
190
+ ...(args.browser_name && { browserName: args.browser_name }),
191
+ ...(args.browser_version && { browserVersion: args.browser_version }),
192
+ ...(args.url && { url: args.url }),
193
+ };
194
+ const out = await services_1.sessionService.startManualSession(body, tokenOrErr);
195
+ if (!out.success) {
196
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
197
+ return (0, helpers_1.errorContent)(out.message);
198
+ }
199
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
200
+ }));
201
+ server.registerTool("sbox_update_session_status", {
202
+ title: "Update Session Status",
203
+ description: "Set a session's result to passed or failed. Uses POST /e34/api/test-data?sessionId=&passed=.",
204
+ inputSchema: {
205
+ token: tokenParamSchema,
206
+ session_id: zod_1.z.string().describe("Session ID (ekey) to update"),
207
+ passed: zod_1.z.boolean().describe("true = passed, false = failed"),
208
+ },
209
+ }, (async (args) => {
210
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
211
+ if (typeof tokenOrErr !== "string")
212
+ return tokenOrErr;
213
+ const out = await services_1.sessionService.updateSessionStatusBySessionId(args.session_id, args.passed, tokenOrErr);
214
+ if (!out.success) {
215
+ (0, helpers_1.clearTokenOnAuthError)(out.message ?? "");
216
+ return (0, helpers_1.errorContent)(out.message ?? "Update failed");
217
+ }
218
+ return (0, helpers_1.textContent)(JSON.stringify({ success: true }));
219
+ }));
220
+ server.registerTool("sbox_list_sessions", {
221
+ title: "List Sessions",
222
+ description: "Show last X sessions (same API as sessions page). Filter by project, status, browser, build; sort by arrival or other field.",
223
+ inputSchema: {
224
+ token: tokenParamSchema,
225
+ limit: zod_1.z.number().optional().describe("Max sessions to return (default 25, max 100)"),
226
+ offset: zod_1.z.number().optional().describe("Pagination offset (default 0)"),
227
+ project_name: zod_1.z.string().optional().describe("Filter by project name"),
228
+ status: zod_1.z.string().optional().describe("Filter by result status (e.g. passed, failed)"),
229
+ browser: zod_1.z.string().optional().describe("Filter by browser name"),
230
+ build: zod_1.z.string().optional().describe("Filter by build name"),
231
+ sort_by: zod_1.z.string().optional().describe("Sort field (default: arrival)"),
232
+ sort_direction: zod_1.z.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
233
+ },
234
+ }, (async (args) => {
235
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
236
+ if (typeof tokenOrErr !== "string")
237
+ return tokenOrErr;
238
+ const out = await services_1.sessionService.listSessions({
239
+ limit: args.limit,
240
+ offset: args.offset,
241
+ projectName: args.project_name,
242
+ status: args.status,
243
+ browser: args.browser,
244
+ build: args.build,
245
+ sortBy: args.sort_by,
246
+ sortDirection: args.sort_direction,
247
+ }, tokenOrErr);
248
+ if (!out.success) {
249
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
250
+ return (0, helpers_1.errorContent)(out.message);
251
+ }
252
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
253
+ }));
254
+ server.registerTool("sbox_delete_session", {
255
+ title: "Delete Manual Session",
256
+ description: "Delete a manual test session by session ID.",
257
+ inputSchema: {
258
+ token: tokenParamSchema,
259
+ session_id: zod_1.z.string().describe("Session ID to delete"),
260
+ },
261
+ }, (async (args) => {
262
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
263
+ if (typeof tokenOrErr !== "string")
264
+ return tokenOrErr;
265
+ const out = await services_1.sessionService.deleteSession(args.session_id, tokenOrErr);
266
+ if (!out.success) {
267
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
268
+ return (0, helpers_1.errorContent)(out.message);
269
+ }
270
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
271
+ }));
272
+ server.registerTool("sbox_upload_mobile_app", {
273
+ title: "Upload Mobile App to SBOX",
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.",
275
+ inputSchema: {
276
+ token: tokenParamSchema,
277
+ project_name: zod_1.z.string().describe("Project name to upload the app to"),
278
+ file_path: zod_1.z.string().describe("Path to the file (APK/IPA); relative to MCP server cwd or absolute"),
279
+ },
280
+ }, (async (args) => {
281
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
282
+ if (typeof tokenOrErr !== "string")
283
+ return tokenOrErr;
284
+ const cwd = process.cwd();
285
+ const resolved = path.isAbsolute(args.file_path) ? path.normalize(args.file_path) : path.resolve(cwd, args.file_path);
286
+ if (!path.isAbsolute(args.file_path) && !resolved.startsWith(cwd)) {
287
+ return (0, helpers_1.errorContent)("Relative file path must be inside current working directory");
288
+ }
289
+ let buffer;
290
+ try {
291
+ buffer = await fs.promises.readFile(resolved);
292
+ }
293
+ catch (e) {
294
+ const msg = e instanceof Error ? e.message : "Failed to read file";
295
+ return (0, helpers_1.errorContent)(msg);
296
+ }
297
+ if (buffer.length > helpers_1.MAX_UPLOAD_SIZE) {
298
+ return (0, helpers_1.errorContent)("File too large (max 200MB)");
299
+ }
300
+ const filename = path.basename(resolved);
301
+ const out = await services_1.uploadService.uploadMobileApp(args.project_name, buffer, filename, tokenOrErr);
302
+ if (!out.success) {
303
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
304
+ return (0, helpers_1.errorContent)(out.message);
305
+ }
306
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
307
+ }));
308
+ const timeRangeSchema = {
309
+ token: tokenParamSchema,
310
+ value: zod_1.z.number().describe("Number of units (e.g. 24 for 24 hours)"),
311
+ unit: zod_1.z.enum(["hour", "minute", "day"]).describe("Time unit: hour, minute, or day"),
312
+ };
313
+ server.registerTool("sbox_sessions_per_browser", {
314
+ title: "Sessions Per Browser",
315
+ description: "Number of sessions ran per browser (and version) in the last X time. Time can be in hours, minutes, or days.",
316
+ inputSchema: timeRangeSchema,
317
+ }, (async (args) => {
318
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
319
+ if (typeof tokenOrErr !== "string")
320
+ return tokenOrErr;
321
+ const out = await services_1.analyticsService.sessionsPerBrowser(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
322
+ if (!out.success) {
323
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
324
+ return (0, helpers_1.errorContent)(out.message);
325
+ }
326
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
327
+ }));
328
+ server.registerTool("sbox_sessions_count_by_builds", {
329
+ title: "Sessions Count By Builds",
330
+ description: "Session counts grouped by build name in the last X time (hours, minutes, or days).",
331
+ inputSchema: timeRangeSchema,
332
+ }, (async (args) => {
333
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
334
+ if (typeof tokenOrErr !== "string")
335
+ return tokenOrErr;
336
+ const out = await services_1.analyticsService.sessionsByBuild(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
337
+ if (!out.success) {
338
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
339
+ return (0, helpers_1.errorContent)(out.message);
340
+ }
341
+ return (0, helpers_1.textContent)(JSON.stringify(out.data, null, 2));
342
+ }));
343
+ server.registerTool("sbox_sessions_failed_count", {
344
+ title: "Sessions Failed Count",
345
+ description: "Number of sessions that failed (or timed out) in the last X time (hours, minutes, or days).",
346
+ inputSchema: timeRangeSchema,
347
+ }, (async (args) => {
348
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
349
+ if (typeof tokenOrErr !== "string")
350
+ return tokenOrErr;
351
+ const out = await services_1.analyticsService.failedCount(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
352
+ if (!out.success) {
353
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
354
+ return (0, helpers_1.errorContent)(out.message);
355
+ }
356
+ return (0, helpers_1.textContent)(JSON.stringify({ failedCount: out.count }));
357
+ }));
358
+ server.registerTool("sbox_most_active_project", {
359
+ title: "Most Active Project",
360
+ description: "Project with the maximum number of sessions in the last X time (hours, minutes, or days).",
361
+ inputSchema: timeRangeSchema,
362
+ }, (async (args) => {
363
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
364
+ if (typeof tokenOrErr !== "string")
365
+ return tokenOrErr;
366
+ const out = await services_1.analyticsService.mostActiveProject(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
367
+ if (!out.success) {
368
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
369
+ return (0, helpers_1.errorContent)(out.message);
370
+ }
371
+ return (0, helpers_1.textContent)(JSON.stringify({ projectName: out.projectName, count: out.count }));
372
+ }));
373
+ server.registerTool("sbox_most_active_user", {
374
+ title: "Most Active User",
375
+ description: "User with the maximum number of sessions in the last X time (hours, minutes, or days).",
376
+ inputSchema: timeRangeSchema,
377
+ }, (async (args) => {
378
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
379
+ if (typeof tokenOrErr !== "string")
380
+ return tokenOrErr;
381
+ const out = await services_1.analyticsService.mostActiveUser(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
382
+ if (!out.success) {
383
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
384
+ return (0, helpers_1.errorContent)(out.message);
385
+ }
386
+ return (0, helpers_1.textContent)(JSON.stringify({ userName: out.userName, count: out.count }));
387
+ }));
388
+ server.registerTool("sbox_total_tests", {
389
+ title: "Total Tests Count",
390
+ description: "Total number of tests (sessions) in the last N days. Matches the Monitoring dashboard total.",
391
+ inputSchema: {
392
+ token: tokenParamSchema,
393
+ days: zod_1.z.number().min(1).max(365).describe("Number of days to count (e.g. 7, 14, 30)"),
394
+ },
395
+ }, (async (args) => {
396
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
397
+ if (typeof tokenOrErr !== "string")
398
+ return tokenOrErr;
399
+ const out = await services_1.analyticsService.totalTests(tokenOrErr, args.days);
400
+ if (!out.success) {
401
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
402
+ return (0, helpers_1.errorContent)(out.message);
403
+ }
404
+ return (0, helpers_1.textContent)(JSON.stringify({ totalTests: out.total, days: args.days }));
405
+ }));
406
+ server.registerTool("sbox_tests_per_test_name", {
407
+ title: "Tests Per Test Name",
408
+ description: "Test counts grouped by test name in the last X time (hours, minutes, or days). Matches Monitoring dashboard.",
409
+ inputSchema: timeRangeSchema,
410
+ }, (async (args) => {
411
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
412
+ if (typeof tokenOrErr !== "string")
413
+ return tokenOrErr;
414
+ const out = await services_1.analyticsService.testsPerTestName(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
415
+ if (!out.success) {
416
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
417
+ return (0, helpers_1.errorContent)(out.message);
418
+ }
419
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
420
+ }));
421
+ server.registerTool("sbox_manual_tests_per_test_name", {
422
+ title: "Manual Tests Per Test Name",
423
+ description: "Manual test counts grouped by test name in the last X time. Matches Monitoring dashboard.",
424
+ inputSchema: timeRangeSchema,
425
+ }, (async (args) => {
426
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
427
+ if (typeof tokenOrErr !== "string")
428
+ return tokenOrErr;
429
+ const out = await services_1.analyticsService.manualTestsPerTestName(tokenOrErr, (0, time_range_1.parseTimeToRange)(args.value, args.unit));
430
+ if (!out.success) {
431
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
432
+ return (0, helpers_1.errorContent)(out.message);
433
+ }
434
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
435
+ }));
436
+ const diagnosticsDaysSchema = {
437
+ token: tokenParamSchema,
438
+ days: zod_1.z.number().min(7).max(365).describe("Number of days (e.g. 7, 14, 30, 90). Min 7, max 365."),
439
+ };
440
+ server.registerTool("sbox_executors_count", {
441
+ title: "Total Executors Count",
442
+ description: "Total number of executors (nodes). Matches Diagnostics dashboard KPI.",
443
+ inputSchema: { token: tokenParamSchema },
444
+ }, (async (args) => {
445
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
446
+ if (typeof tokenOrErr !== "string")
447
+ return tokenOrErr;
448
+ const out = await services_1.diagnosticsService.getExecutorsCount(tokenOrErr);
449
+ if (!out.success) {
450
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
451
+ return (0, helpers_1.errorContent)(out.message);
452
+ }
453
+ return (0, helpers_1.textContent)(JSON.stringify({ count: out.count }));
454
+ }));
455
+ server.registerTool("sbox_cpu_usage_per_node", {
456
+ title: "CPU Usage Per Node",
457
+ description: "CPU usage over time per node for the last N days. Matches Diagnostics dashboard.",
458
+ inputSchema: diagnosticsDaysSchema,
459
+ }, (async (args) => {
460
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
461
+ if (typeof tokenOrErr !== "string")
462
+ return tokenOrErr;
463
+ const out = await services_1.diagnosticsService.getCpuUsagePerNode(tokenOrErr, args.days);
464
+ if (!out.success) {
465
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
466
+ return (0, helpers_1.errorContent)(out.message);
467
+ }
468
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
469
+ }));
470
+ server.registerTool("sbox_tests_queued_hourly", {
471
+ title: "Tests Queued Hourly",
472
+ description: "Queued tests per hour (avg and max) for the last N days. Matches Diagnostics dashboard.",
473
+ inputSchema: diagnosticsDaysSchema,
474
+ }, (async (args) => {
475
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
476
+ if (typeof tokenOrErr !== "string")
477
+ return tokenOrErr;
478
+ const out = await services_1.diagnosticsService.getTestsQueuedHourly(tokenOrErr, args.days);
479
+ if (!out.success) {
480
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
481
+ return (0, helpers_1.errorContent)(out.message);
482
+ }
483
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
484
+ }));
485
+ server.registerTool("sbox_nodes_active_tests", {
486
+ title: "Nodes Active Tests",
487
+ description: "Currently running tests over time (per node) for the last N days. Matches Diagnostics dashboard.",
488
+ inputSchema: diagnosticsDaysSchema,
489
+ }, (async (args) => {
490
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
491
+ if (typeof tokenOrErr !== "string")
492
+ return tokenOrErr;
493
+ const out = await services_1.diagnosticsService.getNodesActiveTests(tokenOrErr, args.days);
494
+ if (!out.success) {
495
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
496
+ return (0, helpers_1.errorContent)(out.message);
497
+ }
498
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
499
+ }));
500
+ server.registerTool("sbox_queue_waiting_time", {
501
+ title: "Queue Waiting Time",
502
+ description: "Queue waiting time (avg and max duration in seconds) over the last N days. Matches Diagnostics dashboard.",
503
+ inputSchema: diagnosticsDaysSchema,
504
+ }, (async (args) => {
505
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
506
+ if (typeof tokenOrErr !== "string")
507
+ return tokenOrErr;
508
+ const out = await services_1.diagnosticsService.getQueueWaitingTime(tokenOrErr, args.days);
509
+ if (!out.success) {
510
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
511
+ return (0, helpers_1.errorContent)(out.message);
512
+ }
513
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
514
+ }));
515
+ server.registerTool("sbox_tests_active_graph", {
516
+ title: "Tests Running Across Executors",
517
+ description: "Tests running across executors over time (by node) for the last N days. Matches Diagnostics dashboard.",
518
+ inputSchema: diagnosticsDaysSchema,
519
+ }, (async (args) => {
520
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
521
+ if (typeof tokenOrErr !== "string")
522
+ return tokenOrErr;
523
+ const out = await services_1.diagnosticsService.getTestsActiveGraph(tokenOrErr, args.days);
524
+ if (!out.success) {
525
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
526
+ return (0, helpers_1.errorContent)(out.message);
527
+ }
528
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
529
+ }));
530
+ server.registerTool("sbox_tests_starting_stats", {
531
+ title: "Tests In Starting State",
532
+ description: "Tests in starting state over time (by node) for the last N days. Matches Diagnostics dashboard.",
533
+ inputSchema: diagnosticsDaysSchema,
534
+ }, (async (args) => {
535
+ const tokenOrErr = (0, helpers_1.requireToken)(args.token);
536
+ if (typeof tokenOrErr !== "string")
537
+ return tokenOrErr;
538
+ const out = await services_1.diagnosticsService.getTestsStartingStats(tokenOrErr, args.days);
539
+ if (!out.success) {
540
+ (0, helpers_1.clearTokenOnAuthError)(out.message);
541
+ return (0, helpers_1.errorContent)(out.message);
542
+ }
543
+ return (0, helpers_1.textContent)(JSON.stringify(out.data));
544
+ }));
545
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MCP protocol server entrypoint. Runs over stdio for Cursor, IntelliJ, Windsurf.
3
+ * Start with: npm run mcp
4
+ * SDK is required by path so Node resolves the CJS .js file (subpath exports can fail when pkg has "type": "module").
5
+ */
6
+ export {};
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * MCP protocol server entrypoint. Runs over stdio for Cursor, IntelliJ, Windsurf.
4
+ * Start with: npm run mcp
5
+ * SDK is required by path so Node resolves the CJS .js file (subpath exports can fail when pkg has "type": "module").
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const path_1 = __importDefault(require("path"));
12
+ const index_1 = require("./mcp/tools/index");
13
+ const sdkServerDir = path_1.default.dirname(require.resolve("@modelcontextprotocol/sdk/server"));
14
+ const { McpServer } = require(path_1.default.join(sdkServerDir, "mcp.js"));
15
+ const { StdioServerTransport } = require(path_1.default.join(sdkServerDir, "stdio.js"));
16
+ const server = new McpServer({
17
+ name: "sbox-mcp",
18
+ version: "1.0.0",
19
+ });
20
+ (0, index_1.registerSboxTools)(server);
21
+ async function main() {
22
+ const transport = new StdioServerTransport();
23
+ await server.connect(transport);
24
+ }
25
+ main().catch((err) => {
26
+ process.stderr.write(`SBOX MCP error: ${err instanceof Error ? err.message : String(err)}\n`);
27
+ process.exit(1);
28
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * JWT auth middleware. Extracts token and attaches to request; does not validate with backend.
3
+ * Validation happens per-request in controllers when calling SBOX APIs.
4
+ */
5
+ import { Request, Response, NextFunction } from "express";
6
+ export interface AuthenticatedRequest extends Request {
7
+ token: string | null;
8
+ }
9
+ export declare function authMiddleware(req: Request, _res: Response, next: NextFunction): void;
10
+ export declare function requireAuth(req: Request, res: Response, next: NextFunction): void;