@seleniumbox/sbox-mcp 0.2.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 +134 -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 +104 -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 +21 -0
  28. package/dist/config/env.js +55 -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 +34 -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 +540 -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 +12 -0
  134. package/dist/token-cache.js +37 -0
  135. package/dist/utils/logger.d.ts +10 -0
  136. package/dist/utils/logger.js +27 -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,62 @@
1
+ /**
2
+ * Session service: get session details, update status, delete session, start manual.
3
+ * Uses adapters and enrichment; never exposes internal IDs.
4
+ */
5
+ import type { SessionAdapter } from "../adapters/session.adapter";
6
+ import type { SessionDetailDto, SessionListResultDto, UpdateSessionStatusBodyDto, DeleteSessionResponseDto, StartManualSessionBodyDto } from "../dto/session.dto";
7
+ export interface ListSessionsParams {
8
+ limit?: number;
9
+ offset?: number;
10
+ projectName?: string;
11
+ status?: string;
12
+ browser?: string;
13
+ build?: string;
14
+ sortBy?: string;
15
+ sortDirection?: string;
16
+ }
17
+ export interface SessionService {
18
+ getSessionDetail(ekey: string, token: string): Promise<{
19
+ success: true;
20
+ data: SessionDetailDto;
21
+ } | {
22
+ success: false;
23
+ status: number;
24
+ message: string;
25
+ }>;
26
+ listSessions(params: ListSessionsParams, token: string): Promise<{
27
+ success: true;
28
+ data: SessionListResultDto;
29
+ } | {
30
+ success: false;
31
+ status: number;
32
+ message: string;
33
+ }>;
34
+ /** Update a single session's result (passed/failed) via POST /e34/api/test-data?sessionId=&passed= */
35
+ updateSessionStatusBySessionId(sessionId: string, passed: boolean, token: string): Promise<{
36
+ success: boolean;
37
+ status: number;
38
+ message?: string;
39
+ }>;
40
+ updateSessionStatus(body: UpdateSessionStatusBodyDto, token: string): Promise<{
41
+ success: boolean;
42
+ status: number;
43
+ message?: string;
44
+ }>;
45
+ deleteSession(sessionId: string, token: string): Promise<{
46
+ success: true;
47
+ data: DeleteSessionResponseDto;
48
+ } | {
49
+ success: false;
50
+ status: number;
51
+ message: string;
52
+ }>;
53
+ startManualSession(body: StartManualSessionBodyDto, token: string): Promise<{
54
+ success: true;
55
+ data: SessionDetailDto;
56
+ } | {
57
+ success: false;
58
+ status: number;
59
+ message: string;
60
+ }>;
61
+ }
62
+ export declare function createSessionService(adapter: SessionAdapter): SessionService;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * Session service: get session details, update status, delete session, start manual.
4
+ * Uses adapters and enrichment; never exposes internal IDs.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createSessionService = createSessionService;
8
+ const enrichment_service_1 = require("./enrichment.service");
9
+ function createSessionService(adapter) {
10
+ return {
11
+ async getSessionDetail(ekey, token) {
12
+ const res = await adapter.getByEkey(ekey, token);
13
+ if (res.status !== 200 || !res.data) {
14
+ return { success: false, status: res.status, message: res.error ?? "Session not found" };
15
+ }
16
+ const videoUrl = (0, enrichment_service_1.buildVideoUrl)(res.data.ekey ?? ekey);
17
+ const dto = (0, enrichment_service_1.enrichSessionDetail)(res.data, videoUrl);
18
+ return { success: true, data: dto };
19
+ },
20
+ async listSessions(params, token) {
21
+ const size = Math.min(Math.max(params.limit ?? 25, 1), 100);
22
+ const offset = Math.max(params.offset ?? 0, 0);
23
+ const res = await adapter.listPaginated({
24
+ projectId: params.projectName,
25
+ size,
26
+ offset,
27
+ status: params.status,
28
+ browser: params.browser,
29
+ build: params.build,
30
+ sortBy: params.sortBy ?? "arrival",
31
+ sortDirection: params.sortDirection ?? "desc",
32
+ }, token);
33
+ if (res.status !== 200) {
34
+ return { success: false, status: res.status, message: res.error ?? "List sessions failed" };
35
+ }
36
+ const content = res.content.map((raw) => {
37
+ const videoUrl = raw.ekey ? (0, enrichment_service_1.buildVideoUrl)(raw.ekey) : null;
38
+ return (0, enrichment_service_1.enrichSessionDetail)(raw, videoUrl);
39
+ });
40
+ return { success: true, data: { content, totalElements: res.totalElements } };
41
+ },
42
+ async updateSessionStatusBySessionId(sessionId, passed, token) {
43
+ const res = await adapter.updateSessionStatus(sessionId, passed, token);
44
+ if (res.status >= 200 && res.status < 300) {
45
+ return { success: true, status: res.status };
46
+ }
47
+ return { success: false, status: res.status, message: res.error };
48
+ },
49
+ async updateSessionStatus(body, token) {
50
+ const res = await adapter.updateStatusByBuild({ buildName: body.buildName, projectId: body.projectName, status: body.status }, token);
51
+ if (res.status >= 200 && res.status < 300) {
52
+ return { success: true, status: res.status };
53
+ }
54
+ return { success: false, status: res.status, message: res.error };
55
+ },
56
+ async deleteSession(sessionId, token) {
57
+ const res = await adapter.deleteManualSession(sessionId, token);
58
+ if (res.status >= 200 && res.status < 300) {
59
+ return { success: true, data: { success: true, message: "Session deleted" } };
60
+ }
61
+ return { success: false, status: res.status, message: res.error ?? "Delete failed" };
62
+ },
63
+ async startManualSession(body, token) {
64
+ const cap = {
65
+ "e34:projectId": body.projectName,
66
+ ...(body.browserName && { browserName: body.browserName }),
67
+ ...(body.browserVersion && { version: body.browserVersion }),
68
+ ...(body.url && { url: body.url }),
69
+ };
70
+ Object.keys(body).forEach((k) => {
71
+ if (!["projectName", "browserName", "browserVersion", "url"].includes(k))
72
+ cap[k] = body[k];
73
+ });
74
+ const res = await adapter.startManualSession(cap, token);
75
+ if (res.status !== 200 || !res.sessionId) {
76
+ return { success: false, status: res.status, message: res.error ?? "Start manual session failed" };
77
+ }
78
+ const getRes = await adapter.getByEkey(res.sessionId, token);
79
+ const raw = getRes.data;
80
+ const videoUrl = (0, enrichment_service_1.buildVideoUrl)(res.sessionId);
81
+ const dto = raw ? (0, enrichment_service_1.enrichSessionDetail)(raw, videoUrl) : {
82
+ sessionId: res.sessionId,
83
+ projectName: body.projectName,
84
+ browserName: body.browserName ?? "",
85
+ browserVersion: body.browserVersion ?? "",
86
+ deviceName: "",
87
+ userName: "",
88
+ status: "running",
89
+ framework: "selenium",
90
+ startTime: new Date().toISOString(),
91
+ endTime: null,
92
+ durationMs: null,
93
+ videoUrl,
94
+ capabilities: {
95
+ browser: body.browserName,
96
+ version: body.browserVersion,
97
+ "e34:projectId": body.projectName,
98
+ ...(body.url && { url: body.url }),
99
+ },
100
+ };
101
+ return { success: true, data: dto };
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Sessions per project (last X days): totalSessions, passed, failed, running, avgDuration.
3
+ * Uses project and session adapters; aggregates in MCP (backend has no single endpoint).
4
+ */
5
+ import type { ProjectAdapter } from "../adapters/project.adapter";
6
+ import type { SessionAdapter } from "../adapters/session.adapter";
7
+ import type { SessionsPerProjectDto } from "../dto/project-stats.dto";
8
+ export interface SessionsPerProjectService {
9
+ getSessionsPerProject(token: string, days: number): Promise<{
10
+ success: true;
11
+ data: SessionsPerProjectDto[];
12
+ } | {
13
+ success: false;
14
+ status: number;
15
+ message: string;
16
+ }>;
17
+ }
18
+ export declare function createSessionsPerProjectService(projectAdapter: ProjectAdapter, sessionAdapter: SessionAdapter): SessionsPerProjectService;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Sessions per project (last X days): totalSessions, passed, failed, running, avgDuration.
4
+ * Uses project and session adapters; aggregates in MCP (backend has no single endpoint).
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createSessionsPerProjectService = createSessionsPerProjectService;
8
+ const PAGE_SIZE = 500;
9
+ function createSessionsPerProjectService(projectAdapter, sessionAdapter) {
10
+ return {
11
+ async getSessionsPerProject(token, days) {
12
+ const countRes = await projectAdapter.getCountPerProject(days, token);
13
+ if (countRes.status !== 200) {
14
+ return { success: false, status: countRes.status, message: countRes.error ?? "Failed to get project counts" };
15
+ }
16
+ const projects = countRes.items.map((p) => p.project).filter(Boolean);
17
+ const results = [];
18
+ const maxAgeDays = Math.max(0, days);
19
+ for (const projectName of projects) {
20
+ const listRes = await sessionAdapter.listPaginated({ projectId: projectName, size: PAGE_SIZE, offset: 0, maxAgeDays }, token);
21
+ if (listRes.status !== 200)
22
+ continue;
23
+ const content = listRes.content;
24
+ let passed = 0;
25
+ let failed = 0;
26
+ let running = 0;
27
+ let totalDuration = 0;
28
+ let countWithDuration = 0;
29
+ for (const s of content) {
30
+ const r = (s.result ?? "").toLowerCase();
31
+ if (r === "passed")
32
+ passed++;
33
+ else if (r === "failed" || r === "timeout")
34
+ failed++;
35
+ else
36
+ running++;
37
+ const d = s.duration ?? s.duration;
38
+ if (typeof d === "number" && d >= 0) {
39
+ totalDuration += d;
40
+ countWithDuration++;
41
+ }
42
+ }
43
+ const totalSessions = content.length;
44
+ const avgDuration = countWithDuration > 0 ? Math.round(totalDuration / countWithDuration) : 0;
45
+ results.push({
46
+ projectName,
47
+ totalSessions,
48
+ passed,
49
+ failed,
50
+ running,
51
+ avgDuration,
52
+ });
53
+ }
54
+ return { success: true, data: results };
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Upload service: upload mobile apps (APK/IPA) to SBOX. Resolves project name to id; no business logic beyond delegation.
3
+ */
4
+ import type { UploadAdapter } from "../adapters/upload.adapter";
5
+ import type { ProjectAdapter } from "../adapters/project.adapter";
6
+ export interface UploadResultDto {
7
+ uploadedAppIdentifier: string;
8
+ projectName: string;
9
+ }
10
+ export interface UploadService {
11
+ uploadMobileApp(projectName: string, fileBuffer: Buffer, filename: string, token: string): Promise<{
12
+ success: true;
13
+ data: UploadResultDto;
14
+ } | {
15
+ success: false;
16
+ status: number;
17
+ message: string;
18
+ }>;
19
+ }
20
+ export declare function createUploadService(uploadAdapter: UploadAdapter, projectAdapter: ProjectAdapter): UploadService;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * Upload service: upload mobile apps (APK/IPA) to SBOX. Resolves project name to id; no business logic beyond delegation.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createUploadService = createUploadService;
7
+ function createUploadService(uploadAdapter, projectAdapter) {
8
+ return {
9
+ async uploadMobileApp(projectName, fileBuffer, filename, token) {
10
+ const listRes = await projectAdapter.listProjects(token);
11
+ if (listRes.status !== 200) {
12
+ return { success: false, status: listRes.status, message: listRes.error ?? "Failed to list projects" };
13
+ }
14
+ const project = listRes.projects.find((p) => (p.name ?? "").toLowerCase() === projectName.toLowerCase());
15
+ if (!project || project.id == null) {
16
+ return { success: false, status: 404, message: `Project not found: ${projectName}` };
17
+ }
18
+ const projectId = String(project.id);
19
+ const res = await uploadAdapter.uploadMobileApp(projectId, fileBuffer, filename, token);
20
+ if (res.status !== 200 || !res.uploadedAppIdentifier) {
21
+ return { success: false, status: res.status, message: res.error ?? "Upload failed" };
22
+ }
23
+ return {
24
+ success: true,
25
+ data: { uploadedAppIdentifier: res.uploadedAppIdentifier, projectName: project.name ?? projectName },
26
+ };
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Active users (top N) with session counts and project names.
3
+ * Uses user adapter and session list; returns business-friendly DTOs.
4
+ */
5
+ import type { SessionAdapter } from "../adapters/session.adapter";
6
+ import type { ActiveUserDto } from "../dto/user.dto";
7
+ export interface UserService {
8
+ getActiveUsersTopN(token: string, limit: number): Promise<{
9
+ success: true;
10
+ data: ActiveUserDto[];
11
+ } | {
12
+ success: false;
13
+ status: number;
14
+ message: string;
15
+ }>;
16
+ }
17
+ export declare function createUserService(sessionAdapter: SessionAdapter): UserService;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * Active users (top N) with session counts and project names.
4
+ * Uses user adapter and session list; returns business-friendly DTOs.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createUserService = createUserService;
8
+ function createUserService(sessionAdapter) {
9
+ return {
10
+ async getActiveUsersTopN(token, limit) {
11
+ const sessionsRes = await sessionAdapter.listPaginated({ size: 500, offset: 0, sortBy: "arrival", sortDirection: "desc" }, token);
12
+ if (sessionsRes.status !== 200) {
13
+ return { success: false, status: sessionsRes.status, message: sessionsRes.error ?? "Failed to list sessions" };
14
+ }
15
+ const byUser = new Map();
16
+ for (const s of sessionsRes.content) {
17
+ const uid = (s.extras ?? {})["e34:userId"];
18
+ const project = (s.extras ?? {})["e34:projectId"];
19
+ const userKey = uid ?? "unknown";
20
+ if (!byUser.has(userKey))
21
+ byUser.set(userKey, { count: 0, projects: new Set() });
22
+ const entry = byUser.get(userKey);
23
+ entry.count += 1;
24
+ if (project)
25
+ entry.projects.add(project);
26
+ }
27
+ const sorted = [...byUser.entries()]
28
+ .sort((a, b) => b[1].count - a[1].count)
29
+ .slice(0, limit);
30
+ const dtos = sorted.map(([userName, { count, projects }]) => ({
31
+ userName,
32
+ email: null,
33
+ activeSessionCount: count,
34
+ projectNames: [...projects],
35
+ }));
36
+ return { success: true, data: dtos };
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * In-memory cache for the SBOX session token. Set on successful sbox_open_login,
3
+ * used by other tools when token is not passed. Cleared on 401/invalid token.
4
+ * lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
5
+ */
6
+ export declare function getCachedToken(): string | null;
7
+ export declare function setCachedToken(token: string): void;
8
+ export declare function clearCachedToken(): void;
9
+ export declare function setLastPollId(pollId: string): void;
10
+ export declare function getLastPollId(): string | null;
11
+ /** Resolve token: prefer provided token, else cached. Returns null if neither. */
12
+ export declare function resolveToken(provided: string | undefined | null): string | null;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * In-memory cache for the SBOX session token. Set on successful sbox_open_login,
4
+ * used by other tools when token is not passed. Cleared on 401/invalid token.
5
+ * lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.getCachedToken = getCachedToken;
9
+ exports.setCachedToken = setCachedToken;
10
+ exports.clearCachedToken = clearCachedToken;
11
+ exports.setLastPollId = setLastPollId;
12
+ exports.getLastPollId = getLastPollId;
13
+ exports.resolveToken = resolveToken;
14
+ let cachedToken = null;
15
+ let lastPollId = null;
16
+ function getCachedToken() {
17
+ return cachedToken;
18
+ }
19
+ function setCachedToken(token) {
20
+ cachedToken = token;
21
+ }
22
+ function clearCachedToken() {
23
+ cachedToken = null;
24
+ }
25
+ function setLastPollId(pollId) {
26
+ lastPollId = pollId;
27
+ }
28
+ function getLastPollId() {
29
+ return lastPollId;
30
+ }
31
+ /** Resolve token: prefer provided token, else cached. Returns null if neither. */
32
+ function resolveToken(provided) {
33
+ const t = (provided ?? "").trim();
34
+ if (t)
35
+ return t;
36
+ return getCachedToken();
37
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Simple logger for MCP module. Can be replaced with a shared SBOX logger if one exists.
3
+ */
4
+ export interface Logger {
5
+ info(msg: string, meta?: Record<string, unknown>): void;
6
+ warn(msg: string, meta?: Record<string, unknown>): void;
7
+ error(msg: string, meta?: Record<string, unknown>): void;
8
+ debug(msg: string, meta?: Record<string, unknown>): void;
9
+ }
10
+ export declare const logger: Logger;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Simple logger for MCP module. Can be replaced with a shared SBOX logger if one exists.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = void 0;
7
+ const isProd = process.env.NODE_ENV === "production";
8
+ function format(level, msg, meta) {
9
+ const ts = new Date().toISOString();
10
+ const m = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
11
+ return `${ts} [${level}] ${msg}${m}`;
12
+ }
13
+ exports.logger = {
14
+ info(msg, meta) {
15
+ console.info(format("INFO", msg, meta));
16
+ },
17
+ warn(msg, meta) {
18
+ console.warn(format("WARN", msg, meta));
19
+ },
20
+ error(msg, meta) {
21
+ console.error(format("ERROR", msg, meta));
22
+ },
23
+ debug(msg, meta) {
24
+ if (!isProd)
25
+ console.debug(format("DEBUG", msg, meta));
26
+ },
27
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Open a URL in the system default browser. Cross-platform via spawn (no shell).
3
+ * Returns true if open succeeded, false otherwise (e.g. headless environment).
4
+ * Never throws; callers can continue (e.g. return auth_url for IDE to open).
5
+ */
6
+ export declare function openUrlInBrowser(url: string): Promise<boolean>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * Open a URL in the system default browser. Cross-platform via spawn (no shell).
4
+ * Returns true if open succeeded, false otherwise (e.g. headless environment).
5
+ * Never throws; callers can continue (e.g. return auth_url for IDE to open).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.openUrlInBrowser = openUrlInBrowser;
9
+ const child_process_1 = require("child_process");
10
+ const os_1 = require("os");
11
+ function openUrlInBrowser(url) {
12
+ return new Promise((resolve) => {
13
+ const plat = (0, os_1.platform)();
14
+ const [cmd, args] = plat === "win32"
15
+ ? ["cmd", ["/c", "start", "", url]]
16
+ : plat === "darwin"
17
+ ? ["open", [url]]
18
+ : ["xdg-open", [url]];
19
+ const child = (0, child_process_1.spawn)(cmd, args, { stdio: "ignore" });
20
+ child.on("error", () => resolve(false));
21
+ child.on("close", (code) => resolve(code === 0));
22
+ });
23
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Builds query string for SBOX time-range params. Backend accepts either days or unit+value.
3
+ */
4
+ export type TimeRange = {
5
+ days: number;
6
+ } | {
7
+ unit: "hour" | "minute" | "day";
8
+ value: number;
9
+ };
10
+ export declare function buildTimeQuery(timeRange: TimeRange): string;
11
+ export declare function parseTimeToRange(value: number, unit: "hour" | "minute" | "day"): TimeRange;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTimeQuery = buildTimeQuery;
4
+ exports.parseTimeToRange = parseTimeToRange;
5
+ function buildTimeQuery(timeRange) {
6
+ if ("days" in timeRange) {
7
+ const days = Math.max(1, Math.min(365, timeRange.days));
8
+ return `days=${days}`;
9
+ }
10
+ const value = Math.max(1, timeRange.value);
11
+ const unit = timeRange.unit;
12
+ return `unit=${encodeURIComponent(unit)}&value=${value}`;
13
+ }
14
+ function parseTimeToRange(value, unit) {
15
+ return { unit, value };
16
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@seleniumbox/sbox-mcp",
3
+ "version": "0.2.0",
4
+ "description": "SBOX MCP – Model Context Protocol server for Selenium Box.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/seleniumbox/selenium-box"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "bin": {
14
+ "sbox-mcp": "dist/index.js"
15
+ },
16
+ "main": "dist/index.js",
17
+ "files": ["dist", "README.md", "LICENSE", "PUBLISHING.md"],
18
+ "scripts": {
19
+ "build": "node --max-old-space-size=4096 ./node_modules/typescript/bin/tsc && node scripts/add-shebang.js",
20
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
21
+ "rebuild": "npm run clean && npm run build",
22
+ "start": "node dist/index.js",
23
+ "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
24
+ "server": "node dist/server.js",
25
+ "dev:server": "ts-node-dev --respawn --transpile-only src/server.ts"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.26.0",
29
+ "axios": "^1.7.7",
30
+ "express": "^4.21.0",
31
+ "form-data": "^4.0.0",
32
+ "multer": "^1.4.5-lts.1",
33
+ "zod": "^3.23.8"
34
+ },
35
+ "devDependencies": {
36
+ "@types/express": "^4.17.21",
37
+ "@types/multer": "^1.4.11",
38
+ "@types/node": "^22.5.0",
39
+ "ts-node-dev": "^2.0.0",
40
+ "typescript": "^5.5.4"
41
+ }
42
+ }