@luxorai/mcp 0.3.1 → 0.4.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 (43) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +13 -8
  3. package/SECURITY.md +2 -2
  4. package/dist/client.d.ts +16 -1
  5. package/dist/client.js +62 -16
  6. package/dist/env.d.ts +6 -0
  7. package/dist/env.js +25 -0
  8. package/dist/http.js +13 -10
  9. package/dist/index.js +9 -8
  10. package/dist/oauth.d.ts +7 -1
  11. package/dist/oauth.js +29 -9
  12. package/dist/server.d.ts +25 -5
  13. package/dist/server.js +88 -53
  14. package/dist/tools/artifacts.d.ts +3 -3
  15. package/dist/tools/artifacts.js +6 -6
  16. package/dist/tools/auth_status.d.ts +3 -3
  17. package/dist/tools/auth_status.js +8 -9
  18. package/dist/tools/common.d.ts +35 -0
  19. package/dist/tools/common.js +78 -0
  20. package/dist/tools/egress.d.ts +38 -0
  21. package/dist/tools/egress.js +153 -0
  22. package/dist/tools/exec.js +1 -1
  23. package/dist/tools/files.js +36 -3
  24. package/dist/tools/files_ops.js +9 -1
  25. package/dist/tools/git.d.ts +0 -1
  26. package/dist/tools/git.js +85 -23
  27. package/dist/tools/ports.d.ts +8 -1
  28. package/dist/tools/ports.js +24 -3
  29. package/dist/tools/previews.js +59 -22
  30. package/dist/tools/sandboxes.d.ts +1 -1
  31. package/dist/tools/sandboxes.js +342 -26
  32. package/dist/tools/sessions_admin.js +63 -23
  33. package/dist/tools/snapshots.js +41 -22
  34. package/dist/tools/ssh.d.ts +3 -3
  35. package/dist/tools/ssh.js +35 -9
  36. package/dist/tools/templates.d.ts +2 -1
  37. package/dist/tools/templates.js +36 -16
  38. package/dist/tools/titles.d.ts +8 -0
  39. package/dist/tools/titles.js +102 -0
  40. package/dist/tools/volumes.js +36 -11
  41. package/dist/tools/workspace.d.ts +9 -1
  42. package/dist/tools/workspace.js +29 -90
  43. package/package.json +5 -4
@@ -11,9 +11,17 @@ async function resolveWorkspaceId(client, provided) {
11
11
  const owner = await client.resolveOwner();
12
12
  return owner.workspaceId;
13
13
  }
14
- /** Workspace-level sandbox usage + default settings (incl. snapshot retention). */
14
+ /**
15
+ * Workspace-level sandbox usage, CI runner sizes and wildcard preview domains.
16
+ *
17
+ * The workspace quota/retention settings RPCs (Get/UpdateWorkspaceSandboxSettings,
18
+ * Get/UpdateWorkspaceSnapshotRetentionSettings) were REMOVED from the API
19
+ * (tenki-app #5007, 2026-08-07; they 404 on staging and production), so their
20
+ * tools are gone. Quotas now come from the workspace plan and are read-only
21
+ * through tenki_get_workspace_usage.
22
+ */
15
23
  export function registerWorkspace(server, client) {
16
- server.tool("tenki_get_workspace_usage", "Get per-second sandbox billing and usage figures for a workspace — use this for cost visibility across all of the workspace's sandboxes.", {
24
+ server.tool("tenki_get_workspace_usage", "Get a workspace's sandbox usage against its plan limits (concurrent sandboxes, snapshots, templates, volumes, storage bytes, preview URLs) — each row has current + max. Use this for quota and cost visibility; the limits themselves come from the plan and are not editable through the API.", {
17
25
  workspace_id: z
18
26
  .string()
19
27
  .optional()
@@ -24,106 +32,37 @@ export function registerWorkspace(server, client) {
24
32
  ...(workspaceId ? { workspaceId } : {}),
25
33
  }));
26
34
  });
27
- server.tool("tenki_get_workspace_settings", "Read a workspace's sandbox quotas and retention policy: max snapshots/templates/volumes/total bytes, max concurrent and sticky sessions, max preview URLs, and the pause/snapshot retention periods. These are workspace limits — there are no per-session defaults (idle timeout and max duration are set per sandbox at creation).", {
35
+ server.tool("tenki_list_ci_runner_sizes", "List the Linux x64 runner sizes Tenki CI can run for a workspace — each runs-on label with its vCPUs and memory in GiB, smallest first, limited to the workspace's job-size limits — and defaultRunsOnLabel, the size a job gets when its runs-on names none. Use it to pick a runs-on label for a GitHub Actions workflow.", {
28
36
  workspace_id: z
29
37
  .string()
30
38
  .optional()
31
- .describe("Workspace to read. Omit to use the API key's first workspace."),
39
+ .describe("Workspace to list sizes for. Omit to use the API key's first workspace."),
32
40
  }, async ({ workspace_id }) => {
33
41
  const workspaceId = await resolveWorkspaceId(client, workspace_id);
34
- return ok(await client.control("GetWorkspaceSandboxSettings", {
35
- ...(workspaceId ? { workspaceId } : {}),
36
- }));
42
+ const resp = await client.control("ListCIRunnerSizes", { ...(workspaceId ? { workspaceId } : {}) });
43
+ // protojson omits empty fields; an empty workspace would otherwise come back as {}.
44
+ return ok({ ...resp, sizes: resp.sizes ?? [], defaultRunsOnLabel: resp.defaultRunsOnLabel ?? "" });
37
45
  });
38
- server.tool("tenki_update_workspace_settings", "Update a workspace's sandbox quotas and retention periods. Only the fields you pass change. Each quota has a matching clear_* flag to remove the override and fall back to the platform default; pass the value OR its clear flag, not both. There are no per-session defaults here — idle timeout and max duration are set per sandbox at creation.", {
46
+ server.tool("tenki_get_workspace_preview_domains", "Read whether wildcard preview domains are enabled for a workspace, the per-region wildcard hostnames and their certificate readiness, and how many existing preview URLs use the wildcard scheme. Explains the `wildcard`/`wildcardStatus` fields on expose/preview responses.", {
39
47
  workspace_id: z
40
48
  .string()
41
49
  .optional()
42
- .describe("Workspace to update. Omit to use the API key's first workspace."),
43
- pause_retention_days: z
44
- .number()
45
- .int()
46
- .positive()
47
- .optional()
48
- .describe("How long a paused sandbox's snapshot is kept before it becomes unresumable."),
49
- clear_pause_retention: z.boolean().optional().describe("Remove the pause-retention override (use the platform default)."),
50
- snapshot_retention_days: z
51
- .number()
52
- .int()
53
- .positive()
54
- .optional()
55
- .describe("How long snapshots are kept before automatic cleanup."),
56
- clear_snapshot_retention: z
57
- .boolean()
58
- .optional()
59
- .describe("Remove the snapshot-retention override, i.e. keep snapshots indefinitely."),
60
- max_snapshots: z.number().int().positive().optional().describe("Maximum snapshots in the workspace."),
61
- max_templates: z.number().int().positive().optional().describe("Maximum templates in the workspace."),
62
- max_volumes: z.number().int().positive().optional().describe("Maximum volumes in the workspace."),
63
- max_total_bytes: z.number().int().positive().optional().describe("Maximum combined snapshot + volume storage in bytes."),
64
- max_concurrent_sessions: z.number().int().positive().optional().describe("Maximum simultaneously active sandboxes."),
65
- max_sticky_sessions: z.number().int().positive().optional().describe("Maximum sticky sandboxes."),
66
- max_preview_urls: z.number().int().positive().optional().describe("Maximum preview URLs in the workspace."),
67
- }, async (a) => {
68
- const workspaceId = await resolveWorkspaceId(client, a.workspace_id);
69
- const body = { ...(workspaceId ? { workspaceId } : {}) };
70
- // Durations are protobuf Duration — JSON wants a seconds string ("2592000s").
71
- if (a.pause_retention_days !== undefined)
72
- body.pauseRetention = `${a.pause_retention_days * 86400}s`;
73
- if (a.clear_pause_retention)
74
- body.clearPauseRetention = true;
75
- if (a.snapshot_retention_days !== undefined)
76
- body.snapshotRetention = `${a.snapshot_retention_days * 86400}s`;
77
- if (a.clear_snapshot_retention)
78
- body.clearSnapshotRetention = true;
79
- if (a.max_snapshots !== undefined)
80
- body.maxSnapshots = a.max_snapshots;
81
- if (a.max_templates !== undefined)
82
- body.maxTemplates = a.max_templates;
83
- if (a.max_volumes !== undefined)
84
- body.maxVolumes = a.max_volumes;
85
- // int64 on the wire — send as a string so large values survive JSON.
86
- if (a.max_total_bytes !== undefined)
87
- body.maxTotalBytes = String(a.max_total_bytes);
88
- if (a.max_concurrent_sessions !== undefined)
89
- body.maxConcurrentSessions = a.max_concurrent_sessions;
90
- if (a.max_sticky_sessions !== undefined)
91
- body.maxStickySessions = a.max_sticky_sessions;
92
- // The per-project field is deprecated; the workspace alias is backed by the same quota.
93
- if (a.max_preview_urls !== undefined)
94
- body.maxPreviewUrlsPerWorkspace = a.max_preview_urls;
95
- if (Object.keys(body).length <= 1) {
96
- throw new Error("tenki_update_workspace_settings: pass at least one setting to change. Nothing was sent — the API would have returned the unchanged settings, which reads like a successful update.");
97
- }
98
- return ok(await client.control("UpdateWorkspaceSandboxSettings", body));
99
- });
100
- server.tool("tenki_get_snapshot_retention_settings", "Get the workspace's pause- and snapshot-retention periods. DEPRECATED upstream: this RPC is marked deprecated in the API — tenki_get_workspace_settings returns the same retention fields alongside the quotas. An empty response means no retention override, i.e. kept indefinitely.", { workspace_id: z.string().optional().describe("Workspace (defaults to the key's first workspace).") }, async ({ workspace_id }) => {
101
- const workspaceId = workspace_id ?? (await client.resolveOwner()).workspaceId;
102
- return ok(await client.control("GetWorkspaceSnapshotRetentionSettings", { ...(workspaceId ? { workspaceId } : {}) }));
50
+ .describe("Workspace to read. Omit to use the API key's first workspace."),
51
+ }, async ({ workspace_id }) => {
52
+ const workspaceId = await resolveWorkspaceId(client, workspace_id);
53
+ return ok(await client.control("GetWorkspacePreviewDomains", { ...(workspaceId ? { workspaceId } : {}) }));
103
54
  });
104
- server.tool("tenki_update_snapshot_retention_settings", "Update the workspace's snapshot-retention policy: how long snapshots are kept before automatic cleanup. Pass retention_days to set it, or clear_retention to keep snapshots indefinitely (the unset state) — exactly one of the two. DEPRECATED upstream: prefer tenki_update_workspace_settings, which sets the same retention (and pause retention) alongside the quotas.", {
105
- retention_days: z
106
- .number()
107
- .int()
108
- .positive()
109
- .optional()
110
- .describe("Days to retain snapshots before automatic cleanup. Omit and pass clear_retention to keep them indefinitely."),
111
- clear_retention: z
112
- .boolean()
55
+ server.tool("tenki_update_workspace_preview_domains", "Enable or disable wildcard preview domains for a workspace. Enabling provisions one wildcard certificate per region and is limited to Pro-tier workspaces. DISABLING releases the certificates and existing wildcard-scheme preview URLs stop serving with a valid certificate — check wildcardPreviewUrlCount via tenki_get_workspace_preview_domains first.", {
56
+ enabled: z.boolean().describe("true to enable wildcard preview domains, false to disable them."),
57
+ workspace_id: z
58
+ .string()
113
59
  .optional()
114
- .describe("Remove the retention period so snapshots are kept indefinitely. Mutually exclusive with retention_days."),
115
- workspace_id: z.string().optional().describe("Workspace (defaults to the key's first workspace)."),
116
- }, async ({ retention_days, clear_retention, workspace_id }) => {
117
- if ((retention_days === undefined) === (clear_retention !== true)) {
118
- throw new Error("tenki_update_snapshot_retention_settings: pass exactly one of retention_days (to set a period) or clear_retention: true (to keep snapshots indefinitely).");
119
- }
120
- const workspaceId = workspace_id ?? (await client.resolveOwner()).workspaceId;
121
- return ok(await client.control("UpdateWorkspaceSnapshotRetentionSettings", {
60
+ .describe("Workspace to update. Omit to use the API key's first workspace."),
61
+ }, async ({ enabled, workspace_id }) => {
62
+ const workspaceId = await resolveWorkspaceId(client, workspace_id);
63
+ return ok(await client.control("UpdateWorkspacePreviewDomains", {
122
64
  ...(workspaceId ? { workspaceId } : {}),
123
- // protobuf Duration on the wire, with a companion clear flag —
124
- // there is no `retentionDays` field (sending one is silently dropped).
125
- ...(retention_days !== undefined ? { snapshotRetention: `${retention_days * 86400}s` } : {}),
126
- ...(clear_retention ? { clearSnapshotRetention: true } : {}),
65
+ enabled,
127
66
  }));
128
67
  });
129
68
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@luxorai/mcp",
3
- "version": "0.3.1",
4
- "mcpName": "io.github.LuxorLabs/tenki-mcp",
3
+ "version": "0.4.0",
4
+ "mcpName": "io.github.LuxorLabs/luxor-mcp",
5
5
  "description": "Model Context Protocol server for Tenki Cloud — disposable microVM sandboxes for AI agents. Create sandboxes, run code, read/write files, run git, expose preview URLs — from any MCP client.",
6
6
  "type": "module",
7
7
  "bin": {
8
- "tenki-mcp": "dist/index.js"
8
+ "tenki-mcp": "dist/index.js",
9
+ "luxor-mcp": "dist/index.js"
9
10
  },
10
11
  "files": [
11
12
  "dist",
@@ -21,7 +22,7 @@
21
22
  "prepack": "npm run build",
22
23
  "prepublishOnly": "npm run build",
23
24
  "pretest": "npm run build",
24
- "test": "node test/offline.test.mjs && node test/public-shapes.test.mjs && node test/exec-output.test.mjs && node test/http-input.test.mjs && node test/oauth-http.test.mjs && node test/security.test.mjs && node test/client-net.test.mjs",
25
+ "test": "node test/offline.test.mjs && node test/presubmission.test.mjs && node test/public-shapes.test.mjs && node test/exec-output.test.mjs && node test/http-input.test.mjs && node test/oauth-http.test.mjs && node test/security.test.mjs && node test/client-net.test.mjs && node test/wire-shapes.test.mjs",
25
26
  "test:all": "npm run build && node test/run.mjs",
26
27
  "test:offline": "npm run build && node test/offline.test.mjs"
27
28
  },