@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,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,5 @@
1
+ /**
2
+ * User controller: get active users (top N). No business logic.
3
+ */
4
+ import { Request, Response } from "express";
5
+ export declare function getActiveUsers(req: Request, res: Response): void;
@@ -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
+ username: string;
8
+ password: string;
9
+ realm?: string | undefined;
10
+ }, {
11
+ username: string;
12
+ password: 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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export * from "./auth.dto";
2
+ export * from "./session.dto";
3
+ export * from "./browser.dto";
4
+ export * from "./device.dto";
5
+ export * from "./user.dto";
6
+ export * from "./project-stats.dto";
@@ -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,8 @@
1
+ export interface SessionsPerProjectDto {
2
+ projectName: string;
3
+ totalSessions: number;
4
+ passed: number;
5
+ failed: number;
6
+ running: number;
7
+ avgDuration: number;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ });
@@ -0,0 +1,6 @@
1
+ export interface ActiveUserDto {
2
+ userName: string;
3
+ email: string | null;
4
+ activeSessionCount: number;
5
+ projectNames: string[];
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entrypoint for @seleniumbox/sbox-mcp. Started by IDE via npx; runs MCP over stdio.
4
+ * Reads SBOX_API (or SBOX_API_BASE_URL) and optional PORT from env. No Docker assumptions.
5
+ */
6
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
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
+ const sdkServerDir = path_1.default.join(__dirname, "..", "node_modules", "@modelcontextprotocol", "sdk", "dist", "cjs", "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
+ });
29
+ process.on("SIGINT", () => {
30
+ process.exit(0);
31
+ });
32
+ process.on("SIGTERM", () => {
33
+ process.exit(0);
34
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Auth MCP tools: sbox_open_login, sbox_complete_login.
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
5
+ export declare function registerAuthTools(server: McpServer): void;
@@ -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 authenticate/log in to SBOX, or when other SBOX tools return 'Not authenticated' or 'Invalid or expired session'. Do NOT call for every SBOX query—use the cached token for other tools.",
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. Token cached for subsequent SBOX tools.",
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; you can use SBOX tools now.",
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>;
@@ -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. Use sbox_open_login to sign in first, then retry.");
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;