@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.
- package/PUBLISHING.md +115 -0
- package/README.md +79 -0
- package/dist/adapters/auth.adapter.d.ts +46 -0
- package/dist/adapters/auth.adapter.js +54 -0
- package/dist/adapters/browser.adapter.d.ts +43 -0
- package/dist/adapters/browser.adapter.js +39 -0
- package/dist/adapters/device.adapter.d.ts +60 -0
- package/dist/adapters/device.adapter.js +40 -0
- package/dist/adapters/diagnostics.adapter.d.ts +73 -0
- package/dist/adapters/diagnostics.adapter.js +89 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.js +20 -0
- package/dist/adapters/project.adapter.d.ts +38 -0
- package/dist/adapters/project.adapter.js +39 -0
- package/dist/adapters/sbox-api.client.d.ts +31 -0
- package/dist/adapters/sbox-api.client.js +166 -0
- package/dist/adapters/session.adapter.d.ts +77 -0
- package/dist/adapters/session.adapter.js +95 -0
- package/dist/adapters/stats.adapter.d.ts +72 -0
- package/dist/adapters/stats.adapter.js +108 -0
- package/dist/adapters/upload.adapter.d.ts +16 -0
- package/dist/adapters/upload.adapter.js +25 -0
- package/dist/adapters/user.adapter.d.ts +24 -0
- package/dist/adapters/user.adapter.js +25 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +16 -0
- package/dist/config/env.d.ts +23 -0
- package/dist/config/env.js +64 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +8 -0
- package/dist/controllers/analytics.controller.d.ts +10 -0
- package/dist/controllers/analytics.controller.js +127 -0
- package/dist/controllers/auth-device.controller.d.ts +7 -0
- package/dist/controllers/auth-device.controller.js +60 -0
- package/dist/controllers/auth.controller.d.ts +5 -0
- package/dist/controllers/auth.controller.js +20 -0
- package/dist/controllers/browser.controller.d.ts +5 -0
- package/dist/controllers/browser.controller.js +23 -0
- package/dist/controllers/device.controller.d.ts +5 -0
- package/dist/controllers/device.controller.js +23 -0
- package/dist/controllers/index.d.ts +6 -0
- package/dist/controllers/index.js +28 -0
- package/dist/controllers/project-stats.controller.d.ts +5 -0
- package/dist/controllers/project-stats.controller.js +29 -0
- package/dist/controllers/session.controller.d.ts +9 -0
- package/dist/controllers/session.controller.js +120 -0
- package/dist/controllers/upload.controller.d.ts +5 -0
- package/dist/controllers/upload.controller.js +44 -0
- package/dist/controllers/user.controller.d.ts +5 -0
- package/dist/controllers/user.controller.js +29 -0
- package/dist/device-flow.store.d.ts +7 -0
- package/dist/device-flow.store.js +23 -0
- package/dist/dto/auth.dto.d.ts +26 -0
- package/dist/dto/auth.dto.js +9 -0
- package/dist/dto/browser.dto.d.ts +13 -0
- package/dist/dto/browser.dto.js +2 -0
- package/dist/dto/device.dto.d.ts +15 -0
- package/dist/dto/device.dto.js +2 -0
- package/dist/dto/index.d.ts +6 -0
- package/dist/dto/index.js +22 -0
- package/dist/dto/project-stats.dto.d.ts +8 -0
- package/dist/dto/project-stats.dto.js +2 -0
- package/dist/dto/session.dto.d.ts +50 -0
- package/dist/dto/session.dto.js +9 -0
- package/dist/dto/user.dto.d.ts +6 -0
- package/dist/dto/user.dto.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +35 -0
- package/dist/mcp/tools/auth-tools.d.ts +5 -0
- package/dist/mcp/tools/auth-tools.js +132 -0
- package/dist/mcp/tools/helpers.d.ts +26 -0
- package/dist/mcp/tools/helpers.js +53 -0
- package/dist/mcp/tools/index.d.ts +5 -0
- package/dist/mcp/tools/index.js +12 -0
- package/dist/mcp/tools/rest-tools.d.ts +5 -0
- package/dist/mcp/tools/rest-tools.js +545 -0
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +28 -0
- package/dist/middleware/auth.middleware.d.ts +10 -0
- package/dist/middleware/auth.middleware.js +23 -0
- package/dist/middleware/debug-log.middleware.d.ts +6 -0
- package/dist/middleware/debug-log.middleware.js +84 -0
- package/dist/middleware/error.middleware.d.ts +8 -0
- package/dist/middleware/error.middleware.js +24 -0
- package/dist/middleware/validate.middleware.d.ts +7 -0
- package/dist/middleware/validate.middleware.js +42 -0
- package/dist/routes/analytics.routes.d.ts +1 -0
- package/dist/routes/analytics.routes.js +15 -0
- package/dist/routes/auth.routes.d.ts +1 -0
- package/dist/routes/auth.routes.js +15 -0
- package/dist/routes/browser.routes.d.ts +1 -0
- package/dist/routes/browser.routes.js +11 -0
- package/dist/routes/device.routes.d.ts +1 -0
- package/dist/routes/device.routes.js +11 -0
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +27 -0
- package/dist/routes/mcp.routes.d.ts +6 -0
- package/dist/routes/mcp.routes.js +70 -0
- package/dist/routes/project-stats.routes.d.ts +1 -0
- package/dist/routes/project-stats.routes.js +11 -0
- package/dist/routes/session.routes.d.ts +1 -0
- package/dist/routes/session.routes.js +23 -0
- package/dist/routes/upload.routes.d.ts +1 -0
- package/dist/routes/upload.routes.js +20 -0
- package/dist/routes/user.routes.d.ts +1 -0
- package/dist/routes/user.routes.js +11 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +17 -0
- package/dist/services/analytics.service.d.ts +88 -0
- package/dist/services/analytics.service.js +98 -0
- package/dist/services/auth.service.d.ts +27 -0
- package/dist/services/auth.service.js +68 -0
- package/dist/services/browser.service.d.ts +25 -0
- package/dist/services/browser.service.js +43 -0
- package/dist/services/device.service.d.ts +18 -0
- package/dist/services/device.service.js +58 -0
- package/dist/services/diagnostics.service.d.ts +87 -0
- package/dist/services/diagnostics.service.js +92 -0
- package/dist/services/enrichment.service.d.ts +9 -0
- package/dist/services/enrichment.service.js +112 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +31 -0
- package/dist/services/project.service.d.ts +22 -0
- package/dist/services/project.service.js +31 -0
- package/dist/services/session.service.d.ts +62 -0
- package/dist/services/session.service.js +104 -0
- package/dist/services/sessions-per-project.service.d.ts +18 -0
- package/dist/services/sessions-per-project.service.js +57 -0
- package/dist/services/upload.service.d.ts +20 -0
- package/dist/services/upload.service.js +29 -0
- package/dist/services/user.service.d.ts +17 -0
- package/dist/services/user.service.js +39 -0
- package/dist/token-cache.d.ts +13 -0
- package/dist/token-cache.js +114 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +31 -0
- package/dist/utils/open-browser.d.ts +6 -0
- package/dist/utils/open-browser.js +23 -0
- package/dist/utils/time-range.d.ts +11 -0
- package/dist/utils/time-range.js +16 -0
- package/package.json +42 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session controller: get, update status, delete, start manual. No business logic.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listSessions = listSessions;
|
|
7
|
+
exports.getSessionDetail = getSessionDetail;
|
|
8
|
+
exports.updateSessionStatus = updateSessionStatus;
|
|
9
|
+
exports.deleteSession = deleteSession;
|
|
10
|
+
exports.startManualSession = startManualSession;
|
|
11
|
+
const services_1 = require("../services");
|
|
12
|
+
const session_dto_1 = require("../dto/session.dto");
|
|
13
|
+
const UpdateBodySchema = session_dto_1.UpdateSessionStatusBodySchema;
|
|
14
|
+
function token(req) {
|
|
15
|
+
return req.token;
|
|
16
|
+
}
|
|
17
|
+
function listSessions(req, res) {
|
|
18
|
+
const t = token(req);
|
|
19
|
+
if (!t) {
|
|
20
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const q = req.query;
|
|
24
|
+
const limit = q.limit != null ? parseInt(q.limit, 10) : undefined;
|
|
25
|
+
const offset = q.offset != null ? parseInt(q.offset, 10) : undefined;
|
|
26
|
+
services_1.sessionService.listSessions({
|
|
27
|
+
limit: isNaN(limit) ? undefined : limit,
|
|
28
|
+
offset: isNaN(offset) ? undefined : offset,
|
|
29
|
+
projectName: q.projectName,
|
|
30
|
+
status: q.status,
|
|
31
|
+
browser: q.browser,
|
|
32
|
+
build: q.build,
|
|
33
|
+
sortBy: q.sortBy,
|
|
34
|
+
sortDirection: q.sortDirection,
|
|
35
|
+
}, t).then((out) => {
|
|
36
|
+
if (out.success) {
|
|
37
|
+
res.status(200).json({ success: true, data: out.data });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
41
|
+
}).catch((err) => {
|
|
42
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function getSessionDetail(req, res) {
|
|
46
|
+
const ekey = req.params.ekey;
|
|
47
|
+
const t = token(req);
|
|
48
|
+
if (!t) {
|
|
49
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
services_1.sessionService.getSessionDetail(ekey, t).then((out) => {
|
|
53
|
+
if (out.success) {
|
|
54
|
+
res.status(200).json({ success: true, data: out.data });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
58
|
+
}).catch((err) => {
|
|
59
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function updateSessionStatus(req, res) {
|
|
63
|
+
const body = UpdateBodySchema.parse(req.body);
|
|
64
|
+
const t = token(req);
|
|
65
|
+
if (!t) {
|
|
66
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
services_1.sessionService.updateSessionStatus(body, t).then((out) => {
|
|
70
|
+
if (out.success) {
|
|
71
|
+
res.status(out.status).json({ success: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
res.status(out.status).json({ success: false, message: out.message ?? "Update failed", code: out.status });
|
|
75
|
+
}).catch((err) => {
|
|
76
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function deleteSession(req, res) {
|
|
80
|
+
const sessionId = req.query.sessionId;
|
|
81
|
+
const t = token(req);
|
|
82
|
+
if (!t) {
|
|
83
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!sessionId) {
|
|
87
|
+
res.status(400).json({ success: false, message: "sessionId required", code: 400 });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
services_1.sessionService.deleteSession(sessionId, t).then((out) => {
|
|
91
|
+
if (out.success) {
|
|
92
|
+
res.status(200).json({ success: true, data: out.data });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function startManualSession(req, res) {
|
|
101
|
+
const body = req.body;
|
|
102
|
+
const t = token(req);
|
|
103
|
+
if (!t) {
|
|
104
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!body || typeof body.projectName !== "string") {
|
|
108
|
+
res.status(400).json({ success: false, message: "projectName is required", code: 400 });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
services_1.sessionService.startManualSession(body, t).then((out) => {
|
|
112
|
+
if (out.success) {
|
|
113
|
+
res.status(200).json({ success: true, data: out.data });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
117
|
+
}).catch((err) => {
|
|
118
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Upload controller: upload mobile app (APK/IPA) to SBOX. No business logic.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadMobileApp = uploadMobileApp;
|
|
7
|
+
const services_1 = require("../services");
|
|
8
|
+
const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
|
|
9
|
+
function token(req) {
|
|
10
|
+
return req.token;
|
|
11
|
+
}
|
|
12
|
+
function uploadMobileApp(req, res) {
|
|
13
|
+
const t = token(req);
|
|
14
|
+
if (!t) {
|
|
15
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const file = req.file;
|
|
19
|
+
if (!file || !file.buffer) {
|
|
20
|
+
res.status(400).json({ success: false, message: "Missing file (use multipart field 'file')", code: 400 });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const projectName = typeof req.body?.projectName === "string" ? req.body.projectName.trim() : "";
|
|
24
|
+
if (!projectName) {
|
|
25
|
+
res.status(400).json({ success: false, message: "Missing projectName in body", code: 400 });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
29
|
+
res.status(400).json({ success: false, message: "File too large (max 200MB)", code: 400 });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
services_1.uploadService
|
|
33
|
+
.uploadMobileApp(projectName, file.buffer, file.originalname || "app", t)
|
|
34
|
+
.then((out) => {
|
|
35
|
+
if (out.success) {
|
|
36
|
+
res.status(200).json({ success: true, data: out.data });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
40
|
+
})
|
|
41
|
+
.catch((err) => {
|
|
42
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* User controller: get active users (top N). No business logic.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getActiveUsers = getActiveUsers;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const services_1 = require("../services");
|
|
9
|
+
const QuerySchema = zod_1.z.object({
|
|
10
|
+
limit: zod_1.z.string().optional().transform((s) => (s ? parseInt(s, 10) : 10)),
|
|
11
|
+
});
|
|
12
|
+
function getActiveUsers(req, res) {
|
|
13
|
+
const token = req.token;
|
|
14
|
+
if (!token) {
|
|
15
|
+
res.status(401).json({ success: false, message: "Authorization required", code: 401 });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const parsed = QuerySchema.safeParse(req.query);
|
|
19
|
+
const limit = parsed.success && !isNaN(parsed.data.limit) ? Math.min(parsed.data.limit, 50) : 10;
|
|
20
|
+
services_1.userService.getActiveUsersTopN(token, limit).then((out) => {
|
|
21
|
+
if (out.success) {
|
|
22
|
+
res.status(200).json({ success: true, data: out.data });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.status(out.status).json({ success: false, message: out.message, code: out.status });
|
|
26
|
+
}).catch((err) => {
|
|
27
|
+
res.status(500).json({ success: false, message: err instanceof Error ? err.message : "Error", code: 500 });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory store for device-flow poll sessions. Tracks poll_id -> deadline for GET /auth/status.
|
|
3
|
+
* Used by POST /auth/initiate and sbox_open_login so the IDE can poll until authenticated or expired.
|
|
4
|
+
*/
|
|
5
|
+
export declare function registerPollSession(pollId: string, expiresInSec?: number): void;
|
|
6
|
+
export declare function isExpired(pollId: string): boolean;
|
|
7
|
+
export declare function hasPollSession(pollId: string): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory store for device-flow poll sessions. Tracks poll_id -> deadline for GET /auth/status.
|
|
4
|
+
* Used by POST /auth/initiate and sbox_open_login so the IDE can poll until authenticated or expired.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.registerPollSession = registerPollSession;
|
|
8
|
+
exports.isExpired = isExpired;
|
|
9
|
+
exports.hasPollSession = hasPollSession;
|
|
10
|
+
const DEFAULT_EXPIRES_IN_SEC = 300;
|
|
11
|
+
const store = new Map();
|
|
12
|
+
function registerPollSession(pollId, expiresInSec = DEFAULT_EXPIRES_IN_SEC) {
|
|
13
|
+
store.set(pollId, Date.now() + expiresInSec * 1000);
|
|
14
|
+
}
|
|
15
|
+
function isExpired(pollId) {
|
|
16
|
+
const deadline = store.get(pollId);
|
|
17
|
+
if (deadline == null)
|
|
18
|
+
return true;
|
|
19
|
+
return Date.now() >= deadline;
|
|
20
|
+
}
|
|
21
|
+
function hasPollSession(pollId) {
|
|
22
|
+
return store.has(pollId);
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const LoginBodySchema: z.ZodObject<{
|
|
3
|
+
username: z.ZodString;
|
|
4
|
+
password: z.ZodString;
|
|
5
|
+
realm: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
password: string;
|
|
8
|
+
username: string;
|
|
9
|
+
realm?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
password: string;
|
|
12
|
+
username: string;
|
|
13
|
+
realm?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type LoginBody = z.infer<typeof LoginBodySchema>;
|
|
16
|
+
/** Customer-facing auth type; do not expose internal realmId. */
|
|
17
|
+
export type AuthType = "Local" | "LDAP" | "OIDC";
|
|
18
|
+
export interface LoginResponseDto {
|
|
19
|
+
token: string;
|
|
20
|
+
principal: {
|
|
21
|
+
userId: string;
|
|
22
|
+
fullname?: string;
|
|
23
|
+
/** Customer-friendly: Local, LDAP, or OIDC. Internal realm id is not exposed. */
|
|
24
|
+
authType: AuthType;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoginBodySchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.LoginBodySchema = zod_1.z.object({
|
|
6
|
+
username: zod_1.z.string().min(1, "username is required"),
|
|
7
|
+
password: zod_1.z.string().min(1, "password is required"),
|
|
8
|
+
realm: zod_1.z.string().optional(),
|
|
9
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BrowserDto {
|
|
2
|
+
browserName: string;
|
|
3
|
+
version: string;
|
|
4
|
+
platform: string;
|
|
5
|
+
capabilities: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** Playwright version with bundled browser versions (from GET /e34/api/playwright) */
|
|
8
|
+
export interface PlaywrightVersionDto {
|
|
9
|
+
version: string;
|
|
10
|
+
chromium: string;
|
|
11
|
+
webkit: string;
|
|
12
|
+
firefox: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DeviceDto {
|
|
2
|
+
/** Short display name (e.g. "Galaxy S21 GPS") */
|
|
3
|
+
deviceName: string;
|
|
4
|
+
/** Full name (e.g. "Samsung Galaxy S21 GPS") */
|
|
5
|
+
completeDeviceName?: string;
|
|
6
|
+
/** AVD/emulator identifier (Android) */
|
|
7
|
+
avdName?: string;
|
|
8
|
+
/** Platform: Android, iOS, or MobileWeb */
|
|
9
|
+
os: string;
|
|
10
|
+
/** OS version(s) - single string or from supportedPlatformVersions */
|
|
11
|
+
version: string;
|
|
12
|
+
/** Supported platform versions (e.g. ["13.0"]) */
|
|
13
|
+
supportedPlatformVersions?: string[];
|
|
14
|
+
availability?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth.dto"), exports);
|
|
18
|
+
__exportStar(require("./session.dto"), exports);
|
|
19
|
+
__exportStar(require("./browser.dto"), exports);
|
|
20
|
+
__exportStar(require("./device.dto"), exports);
|
|
21
|
+
__exportStar(require("./user.dto"), exports);
|
|
22
|
+
__exportStar(require("./project-stats.dto"), exports);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface SessionDetailDto {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
projectName: string;
|
|
4
|
+
browserName: string;
|
|
5
|
+
browserVersion: string;
|
|
6
|
+
deviceName: string;
|
|
7
|
+
userName: string;
|
|
8
|
+
status: string;
|
|
9
|
+
framework: string;
|
|
10
|
+
startTime: string | null;
|
|
11
|
+
endTime: string | null;
|
|
12
|
+
durationMs: number | null;
|
|
13
|
+
videoUrl: string | null;
|
|
14
|
+
/** All capabilities sent to the session (e34:* extras + top-level session fields). */
|
|
15
|
+
capabilities: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface SessionListResultDto {
|
|
18
|
+
content: SessionDetailDto[];
|
|
19
|
+
totalElements: number;
|
|
20
|
+
}
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
export declare const UpdateSessionStatusBodySchema: z.ZodObject<{
|
|
23
|
+
buildName: z.ZodString;
|
|
24
|
+
projectName: z.ZodString;
|
|
25
|
+
status: z.ZodEnum<["passed", "failed", "timeout"]>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
status: "timeout" | "passed" | "failed";
|
|
28
|
+
buildName: string;
|
|
29
|
+
projectName: string;
|
|
30
|
+
}, {
|
|
31
|
+
status: "timeout" | "passed" | "failed";
|
|
32
|
+
buildName: string;
|
|
33
|
+
projectName: string;
|
|
34
|
+
}>;
|
|
35
|
+
export interface UpdateSessionStatusBodyDto {
|
|
36
|
+
buildName: string;
|
|
37
|
+
projectName: string;
|
|
38
|
+
status: "passed" | "failed" | "timeout";
|
|
39
|
+
}
|
|
40
|
+
export interface DeleteSessionResponseDto {
|
|
41
|
+
success: boolean;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
export interface StartManualSessionBodyDto {
|
|
45
|
+
projectName: string;
|
|
46
|
+
browserName?: string;
|
|
47
|
+
browserVersion?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UpdateSessionStatusBodySchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.UpdateSessionStatusBodySchema = zod_1.z.object({
|
|
6
|
+
buildName: zod_1.z.string().min(1),
|
|
7
|
+
projectName: zod_1.z.string().min(1),
|
|
8
|
+
status: zod_1.z.enum(["passed", "failed", "timeout"]),
|
|
9
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* CLI entrypoint for @seleniumbox/sbox-mcp. Started by IDE via npx; runs MCP over stdio.
|
|
5
|
+
* Reads SBOX_API (or SBOX_API_BASE_URL) and optional PORT from env. No Docker assumptions.
|
|
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
|
+
// Resolve SDK server dir via the package's "server" export (dist/cjs/server) so it works when npx hoists deps
|
|
14
|
+
const sdkServerDir = path_1.default.dirname(require.resolve("@modelcontextprotocol/sdk/server"));
|
|
15
|
+
const { McpServer } = require(path_1.default.join(sdkServerDir, "mcp.js"));
|
|
16
|
+
const { StdioServerTransport } = require(path_1.default.join(sdkServerDir, "stdio.js"));
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: "sbox-mcp",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
});
|
|
21
|
+
(0, index_1.registerSboxTools)(server);
|
|
22
|
+
async function main() {
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
|
26
|
+
main().catch((err) => {
|
|
27
|
+
process.stderr.write(`SBOX MCP error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
process.on("SIGINT", () => {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
process.on("SIGTERM", () => {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth MCP tools: sbox_open_login, sbox_complete_login.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerAuthTools = registerAuthTools;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const services_1 = require("../../services");
|
|
10
|
+
const open_browser_1 = require("../../utils/open-browser");
|
|
11
|
+
const config_1 = require("../../config");
|
|
12
|
+
const device_flow_store_1 = require("../../device-flow.store");
|
|
13
|
+
const token_cache_1 = require("../../token-cache");
|
|
14
|
+
const helpers_1 = require("./helpers");
|
|
15
|
+
const DEVICE_FLOW_EXPIRES_IN_SEC = 300;
|
|
16
|
+
const POLL_INTERVAL_MS = 3000;
|
|
17
|
+
const POLL_TIMEOUT_MS = 60000;
|
|
18
|
+
const BACKGROUND_POLL_TIMEOUT_MS = 300000;
|
|
19
|
+
const COMPLETE_LOGIN_POLL_MS = 60000;
|
|
20
|
+
function startBackgroundPollForToken(pollId) {
|
|
21
|
+
const deadline = Date.now() + BACKGROUND_POLL_TIMEOUT_MS;
|
|
22
|
+
const poll = async () => {
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
const token = await services_1.authService.pollForToken(pollId);
|
|
25
|
+
if (token) {
|
|
26
|
+
const out = await services_1.authService.validateToken(token);
|
|
27
|
+
if (out.success)
|
|
28
|
+
(0, token_cache_1.setCachedToken)(token);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await (0, helpers_1.sleep)(POLL_INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
poll().catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
function registerAuthTools(server) {
|
|
37
|
+
server.registerTool("sbox_open_login", {
|
|
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 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
|
+
inputSchema: {},
|
|
41
|
+
}, (async () => {
|
|
42
|
+
try {
|
|
43
|
+
const enabled = await services_1.authService.isMcpAddonEnabled();
|
|
44
|
+
if (!enabled) {
|
|
45
|
+
return (0, helpers_1.errorContent)("MCP add-on is not configured or not enabled on this SBOX hub. Please enable the MCP add-on to log in via the browser.");
|
|
46
|
+
}
|
|
47
|
+
const pollId = (0, crypto_1.randomUUID)();
|
|
48
|
+
const authUrl = (0, config_1.getPublicLoginPageUrl)() + "?mcp_poll_id=" + encodeURIComponent(pollId);
|
|
49
|
+
(0, device_flow_store_1.registerPollSession)(pollId, DEVICE_FLOW_EXPIRES_IN_SEC);
|
|
50
|
+
const opened = await (0, open_browser_1.openUrlInBrowser)(authUrl);
|
|
51
|
+
if (!opened) {
|
|
52
|
+
(0, token_cache_1.setLastPollId)(pollId);
|
|
53
|
+
startBackgroundPollForToken(pollId);
|
|
54
|
+
return (0, helpers_1.textContent)(JSON.stringify({
|
|
55
|
+
auth_url: authUrl,
|
|
56
|
+
poll_id: pollId,
|
|
57
|
+
expires_in: DEVICE_FLOW_EXPIRES_IN_SEC,
|
|
58
|
+
message: "Open the URL above in your browser to sign in. The IDE may open it automatically. After signing in, you can use SBOX tools; if you see 'not authenticated', say 'complete SBOX login' with the same session or try again in a few seconds.",
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
62
|
+
let token = null;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
token = await services_1.authService.pollForToken(pollId);
|
|
65
|
+
if (token)
|
|
66
|
+
break;
|
|
67
|
+
await (0, helpers_1.sleep)(POLL_INTERVAL_MS);
|
|
68
|
+
}
|
|
69
|
+
if (token) {
|
|
70
|
+
const out = await services_1.authService.validateToken(token);
|
|
71
|
+
if (!out.success)
|
|
72
|
+
return (0, helpers_1.errorContent)(out.message);
|
|
73
|
+
(0, token_cache_1.setCachedToken)(token);
|
|
74
|
+
const name = out.data.principal?.fullname || out.data.principal?.userId || "—";
|
|
75
|
+
const authType = out.data.principal?.authType || "Local";
|
|
76
|
+
return (0, helpers_1.textContent)(JSON.stringify({
|
|
77
|
+
success: true,
|
|
78
|
+
token,
|
|
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
|
+
principal: { name, authType },
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
return (0, helpers_1.textContent)(JSON.stringify({
|
|
84
|
+
success: false,
|
|
85
|
+
auth_url: authUrl,
|
|
86
|
+
message: "Sign-in did not complete within 1 minute. The login link is still valid for 5 minutes. Open the URL below in your browser to sign in, then use SBOX tools again.",
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
const msg = e instanceof Error ? e.message : "Something went wrong while initiating login.";
|
|
91
|
+
return (0, helpers_1.errorContent)(msg);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
server.registerTool("sbox_complete_login", {
|
|
95
|
+
title: "Complete SBOX Login (after signing in in browser)",
|
|
96
|
+
description: "Call after the user has signed in in the browser. Polls for the token by poll_id and caches it so subsequent SBOX tools work. Pass the poll_id from the previous sbox_open_login response, or omit to use the last poll_id.",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
poll_id: zod_1.z.string().optional().describe("Poll ID from sbox_open_login response; omit to use last poll"),
|
|
99
|
+
},
|
|
100
|
+
}, (async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
const pollId = (args.poll_id ?? (0, token_cache_1.getLastPollId)() ?? "").trim();
|
|
103
|
+
if (!pollId) {
|
|
104
|
+
return (0, helpers_1.errorContent)("No poll_id provided and no recent login session. Run sbox_open_login first, then open the URL and sign in, then call this tool with the poll_id from that response.");
|
|
105
|
+
}
|
|
106
|
+
const deadline = Date.now() + COMPLETE_LOGIN_POLL_MS;
|
|
107
|
+
let token = null;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
token = await services_1.authService.pollForToken(pollId);
|
|
110
|
+
if (token)
|
|
111
|
+
break;
|
|
112
|
+
await (0, helpers_1.sleep)(POLL_INTERVAL_MS);
|
|
113
|
+
}
|
|
114
|
+
if (!token) {
|
|
115
|
+
return (0, helpers_1.errorContent)("Token not received yet. Make sure you completed sign-in in the browser, then try again.");
|
|
116
|
+
}
|
|
117
|
+
const out = await services_1.authService.validateToken(token);
|
|
118
|
+
if (!out.success)
|
|
119
|
+
return (0, helpers_1.errorContent)(out.message);
|
|
120
|
+
(0, token_cache_1.setCachedToken)(token);
|
|
121
|
+
const name = out.data.principal?.fullname || out.data.principal?.userId || "—";
|
|
122
|
+
return (0, helpers_1.textContent)(JSON.stringify({
|
|
123
|
+
success: true,
|
|
124
|
+
message: "SBOX login complete. Token cached and will be used for all SBOX API calls until expiry (~1h).",
|
|
125
|
+
principal: { name },
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
return (0, helpers_1.errorContent)(e instanceof Error ? e.message : "Failed to complete login.");
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for MCP tool responses and token handling.
|
|
3
|
+
*/
|
|
4
|
+
export declare const MAX_UPLOAD_SIZE: number;
|
|
5
|
+
export declare function textContent(text: string): {
|
|
6
|
+
content: Array<{
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export declare function friendlyError(message: string): string;
|
|
12
|
+
export declare function errorContent(message: string): {
|
|
13
|
+
content: Array<{
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
export declare function requireToken(provided: string | undefined): string | {
|
|
19
|
+
content: Array<{
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
export declare function isAuthError(message: string): boolean;
|
|
25
|
+
export declare function clearTokenOnAuthError(message: string): void;
|
|
26
|
+
export declare function sleep(ms: number): Promise<void>;
|