@tenkicloud/mcp 0.1.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/CHANGELOG.md +161 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/SECURITY.md +50 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.js +499 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +234 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +59 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.js +205 -0
- package/dist/tools/artifacts.d.ts +16 -0
- package/dist/tools/artifacts.js +19 -0
- package/dist/tools/auth_status.d.ts +35 -0
- package/dist/tools/auth_status.js +104 -0
- package/dist/tools/common.d.ts +33 -0
- package/dist/tools/common.js +42 -0
- package/dist/tools/exec.d.ts +4 -0
- package/dist/tools/exec.js +88 -0
- package/dist/tools/files.d.ts +4 -0
- package/dist/tools/files.js +22 -0
- package/dist/tools/files_ops.d.ts +12 -0
- package/dist/tools/files_ops.js +54 -0
- package/dist/tools/git.d.ts +4 -0
- package/dist/tools/git.js +30 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +5 -0
- package/dist/tools/ports.d.ts +4 -0
- package/dist/tools/ports.js +6 -0
- package/dist/tools/previews.d.ts +18 -0
- package/dist/tools/previews.js +102 -0
- package/dist/tools/registry.d.ts +18 -0
- package/dist/tools/registry.js +98 -0
- package/dist/tools/run.d.ts +4 -0
- package/dist/tools/run.js +11 -0
- package/dist/tools/sandboxes.d.ts +4 -0
- package/dist/tools/sandboxes.js +66 -0
- package/dist/tools/sessions_admin.d.ts +9 -0
- package/dist/tools/sessions_admin.js +76 -0
- package/dist/tools/snapshots.d.ts +11 -0
- package/dist/tools/snapshots.js +91 -0
- package/dist/tools/ssh.d.ts +15 -0
- package/dist/tools/ssh.js +17 -0
- package/dist/tools/templates.d.ts +14 -0
- package/dist/tools/templates.js +151 -0
- package/dist/tools/volumes.d.ts +16 -0
- package/dist/tools/volumes.js +94 -0
- package/dist/tools/workspace.d.ts +4 -0
- package/dist/tools/workspace.js +129 -0
- package/package.json +61 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "./common.js";
|
|
3
|
+
const VISIBILITY = { public: "REGISTRY_VISIBILITY_PUBLIC", private: "REGISTRY_VISIBILITY_PRIVATE" };
|
|
4
|
+
const IMAGE_KIND = { snapshot: "REGISTRY_IMAGE_KIND_SNAPSHOT", template: "REGISTRY_IMAGE_KIND_TEMPLATE" };
|
|
5
|
+
export function registerRegistry(server, client) {
|
|
6
|
+
// ── Publish ─────────────────────────────────────────────────────────────────
|
|
7
|
+
server.tool("tenki_publish_image", "Publish a custom sandbox image into the workspace registry from a snapshot or a template.", {
|
|
8
|
+
reference: z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^[^:@]+$/, "publish takes the TAGLESS form <workspace>/<artifact> — the API rejects a tag or @snapshot here")
|
|
11
|
+
.describe("Target image reference in TAGLESS form <workspace>/<artifact>, e.g. myws/myimage — the API rejects a tag or @snapshot on publish."),
|
|
12
|
+
kind: z.enum(["snapshot", "template"]).describe("Source kind for the image contents."),
|
|
13
|
+
snapshot_id: z.string().optional().describe("Snapshot id to publish (required when kind=snapshot)."),
|
|
14
|
+
source_template_id: z.string().optional().describe("Template id to publish (required when kind=template)."),
|
|
15
|
+
visibility: z.enum(["public", "private"]).optional().describe("public (resolvable by anyone) or private (default)."),
|
|
16
|
+
}, async ({ reference, kind, snapshot_id, source_template_id, visibility }) => ok(await client.control("PublishRegistryImage", {
|
|
17
|
+
ref: reference,
|
|
18
|
+
kind: IMAGE_KIND[kind],
|
|
19
|
+
...(snapshot_id !== undefined ? { snapshotId: snapshot_id } : {}),
|
|
20
|
+
...(source_template_id !== undefined ? { sourceTemplateId: source_template_id } : {}),
|
|
21
|
+
...(visibility !== undefined ? { visibility: VISIBILITY[visibility] } : {}),
|
|
22
|
+
})));
|
|
23
|
+
// ── Get ─────────────────────────────────────────────────────────────────────
|
|
24
|
+
server.tool("tenki_get_image", "Retrieve one custom sandbox image from the registry by its reference.", { reference: z.string().describe("Image reference <workspace>/<artifact>[:tag].") }, async ({ reference }) => ok(await client.control("GetRegistryImage", { ref: reference })));
|
|
25
|
+
// ── List ────────────────────────────────────────────────────────────────────
|
|
26
|
+
server.tool("tenki_list_images", "List custom sandbox images in the registry, optionally filtered to a single workspace.", {
|
|
27
|
+
workspace_id: z.string().optional().describe("Optional workspace to filter the listed images by."),
|
|
28
|
+
page_size: z.number().int().positive().optional().describe("Max images to return per page."),
|
|
29
|
+
page_token: z.string().optional().describe("Pagination token from a previous response's nextPageToken."),
|
|
30
|
+
}, async ({ workspace_id, page_size, page_token }) => ok(await client.control("ListRegistryImages", {
|
|
31
|
+
...(workspace_id !== undefined ? { workspaceId: workspace_id } : {}),
|
|
32
|
+
...(page_size !== undefined ? { pageSize: page_size } : {}),
|
|
33
|
+
...(page_token !== undefined ? { pageToken: page_token } : {}),
|
|
34
|
+
})));
|
|
35
|
+
// ── Set visibility ────────────────────────────────────────────────────────────
|
|
36
|
+
server.tool("tenki_set_image_visibility", "Make a custom sandbox image public (publicly resolvable) or private (restricted to the workspace).", {
|
|
37
|
+
reference: z.string().describe("Image reference <workspace>/<artifact>[:tag]."),
|
|
38
|
+
visibility: z.enum(["public", "private"]).describe("Target visibility for the image."),
|
|
39
|
+
}, async ({ reference, visibility }) => ok(await client.control("SetRegistryImageVisibility", { ref: reference, visibility: VISIBILITY[visibility] })));
|
|
40
|
+
// ── Delete (whole image by ref, or one version by ids) ─────────────────────────
|
|
41
|
+
server.tool("tenki_delete_image", "Delete a custom sandbox image (by reference), or delete a single version (by image_id + snapshot_id).", {
|
|
42
|
+
reference: z.string().optional().describe("Image reference to delete the whole image."),
|
|
43
|
+
image_id: z.string().optional().describe("Image UUID (with snapshot_id) to delete a single version."),
|
|
44
|
+
snapshot_id: z.string().optional().describe("Snapshot UUID (with image_id) to delete a single version."),
|
|
45
|
+
}, async ({ reference, image_id, snapshot_id }) => {
|
|
46
|
+
if (image_id !== undefined && snapshot_id !== undefined) {
|
|
47
|
+
return ok(await client.control("DeleteRegistryImageVersion", { imageId: image_id, snapshotId: snapshot_id }));
|
|
48
|
+
}
|
|
49
|
+
if (reference !== undefined)
|
|
50
|
+
return ok(await client.control("DeleteRegistryImage", { ref: reference }));
|
|
51
|
+
throw new Error("Provide `reference` to delete an image, or `image_id`+`snapshot_id` to delete one version.");
|
|
52
|
+
});
|
|
53
|
+
// ── Resolve ref ────────────────────────────────────────────────────────────────
|
|
54
|
+
server.tool("tenki_resolve_image_ref", "Resolve a registry reference (tag or ref) to its concrete pinned digest/ref.", { registry_ref: z.string().describe("The registry reference to resolve, e.g. myws/myimage:latest.") }, async ({ registry_ref }) => {
|
|
55
|
+
const owner = await client.resolveOwner();
|
|
56
|
+
return ok(await client.control("ResolveRegistryRef", {
|
|
57
|
+
ref: registry_ref,
|
|
58
|
+
...(owner.workspaceId ? { workspaceId: owner.workspaceId } : {}),
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
// ── Share ──────────────────────────────────────────────────────────────────────
|
|
62
|
+
server.tool("tenki_share_image", "Grant another workspace access to a custom sandbox image.", {
|
|
63
|
+
reference: z.string().describe("Image reference <workspace>/<artifact>[:tag]."),
|
|
64
|
+
grantee_workspace_id: z.string().describe("The workspace to grant access to."),
|
|
65
|
+
},
|
|
66
|
+
// ShareImage uses imageRef + targetWorkspaceId (not ref/granteeWorkspaceId).
|
|
67
|
+
async ({ reference, grantee_workspace_id }) => ok(await client.control("ShareImage", { imageRef: reference, targetWorkspaceId: grantee_workspace_id })));
|
|
68
|
+
// ── Unshare (revoke a share) ────────────────────────────────────────────────────
|
|
69
|
+
server.tool("tenki_unshare_image", "Revoke a previously-granted share on a custom sandbox image, by grant id or grantee workspace.", {
|
|
70
|
+
reference: z.string().describe("Image reference (use the TAGLESS <workspace>/<artifact> form)."),
|
|
71
|
+
grant_id: z.string().optional().describe("Specific share grant to revoke (preferred; provide this or grantee_workspace_id)."),
|
|
72
|
+
grantee_workspace_id: z.string().optional().describe("Workspace whose access to revoke."),
|
|
73
|
+
}, async ({ reference, grant_id, grantee_workspace_id }) => {
|
|
74
|
+
// Without one of these the server revokes nothing and still returns the
|
|
75
|
+
// image object, which reads as a successful revoke. Mirror the guard
|
|
76
|
+
// tenki_delete_image already applies to its own one-of.
|
|
77
|
+
if (!grant_id && !grantee_workspace_id) {
|
|
78
|
+
throw new Error("tenki_unshare_image: pass grant_id (preferred) or grantee_workspace_id. With neither, the API revokes nothing and returns the image unchanged, which looks like success.");
|
|
79
|
+
}
|
|
80
|
+
return ok(await client.control("UnshareRegistryImage", {
|
|
81
|
+
ref: reference,
|
|
82
|
+
...(grant_id !== undefined ? { grantId: grant_id } : {}),
|
|
83
|
+
...(grantee_workspace_id !== undefined ? { targetWorkspaceId: grantee_workspace_id } : {}),
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
// ── List share grants ───────────────────────────────────────────────────────────
|
|
87
|
+
server.tool("tenki_list_image_share_grants", "List the share grants (workspaces granted access) on a custom sandbox image.", {
|
|
88
|
+
reference: z.string().describe("Image reference to list grants for (use the TAGLESS <workspace>/<artifact> form)."),
|
|
89
|
+
page_size: z.number().int().positive().optional().describe("Max grants to return per page."),
|
|
90
|
+
page_token: z.string().optional().describe("Pagination token from a previous response's nextPageToken."),
|
|
91
|
+
}, async ({ reference, page_size, page_token }) => ok(await client.control("ListRegistryShareGrants", {
|
|
92
|
+
ref: reference,
|
|
93
|
+
...(page_size !== undefined ? { pageSize: page_size } : {}),
|
|
94
|
+
...(page_token !== undefined ? { pageToken: page_token } : {}),
|
|
95
|
+
})));
|
|
96
|
+
// ── Revoke a specific share grant (by grant id) ─────────────────────────────────
|
|
97
|
+
server.tool("tenki_revoke_image_share_grant", "Revoke a specific registry-image share grant by its grant id.", { grant_id: z.string().describe("The share grant id (UUID) to revoke.") }, async ({ grant_id }) => ok(await client.control("RevokeRegistryShareGrant", { grantId: grant_id })));
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, envSchema } from "./common.js";
|
|
3
|
+
/** The headline one-shot execution tool. */
|
|
4
|
+
export function registerRun(server, client) {
|
|
5
|
+
server.tool("tenki_run_code", "Boot a throwaway microVM, run a snippet (shell/python/javascript), return its stdout/stderr/exit code, and tear the sandbox down. Cost-guarded and self-terminating. Use this for one-shot execution when you don't need a persistent sandbox.", {
|
|
6
|
+
language: z.enum(["shell", "python", "javascript"]).describe("Interpreter for the snippet."),
|
|
7
|
+
code: z.string().describe("The code to run."),
|
|
8
|
+
env: envSchema,
|
|
9
|
+
timeout_seconds: z.number().int().positive().optional().describe("Max seconds for the run (default 30)."),
|
|
10
|
+
}, async ({ language, code, env, timeout_seconds }) => ok(await client.runCode(language, code, { env, timeoutSeconds: timeout_seconds })));
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, envSchema, sessionIdSchema } from "./common.js";
|
|
3
|
+
/** Sandbox (session) lifecycle. */
|
|
4
|
+
export function registerSandboxes(server, client) {
|
|
5
|
+
server.tool("tenki_create_sandbox", "Create a persistent sandbox microVM. Returns the session (id, state) and its data-plane endpoint. Boots in ~2s. Use tenki_exec / tenki_read_file / tenki_write_file against the returned session_id.", {
|
|
6
|
+
name: z.string().optional().describe("Human-readable name."),
|
|
7
|
+
cpu_cores: z.number().int().min(1).max(16).optional().describe("vCPUs (default 2)."),
|
|
8
|
+
memory_mb: z.number().int().min(128).max(65536).optional().describe("Memory in MB (default 4096)."),
|
|
9
|
+
disk_size_gb: z.number().int().positive().optional().describe("Disk in GB (default 5)."),
|
|
10
|
+
max_duration_seconds: z.number().int().positive().optional().describe("Hard lifetime cap in seconds."),
|
|
11
|
+
idle_timeout_minutes: z.number().int().positive().optional().describe("Reap after N idle minutes."),
|
|
12
|
+
clone_repo_url: z.string().optional().describe("Git URL to clone into the sandbox on boot."),
|
|
13
|
+
allow_outbound: z.boolean().optional().describe("Allow outbound networking (off by default)."),
|
|
14
|
+
allow_inbound: z.boolean().optional().describe("Allow inbound networking (off by default)."),
|
|
15
|
+
snapshot_id: z.string().optional().describe("Boot from a snapshot."),
|
|
16
|
+
registry_ref: z.string().optional().describe("Boot from a custom registry image."),
|
|
17
|
+
tags: z.array(z.string()).optional().describe("Tags for later filtering."),
|
|
18
|
+
project_id: z.string().optional().describe("Project to create in (defaults to the key's first project)."),
|
|
19
|
+
workspace_id: z.string().optional().describe("Workspace to create in (defaults to the key's first workspace)."),
|
|
20
|
+
env: envSchema,
|
|
21
|
+
wait_ready: z.boolean().optional().describe("Poll until the sandbox is RUNNING before returning (default true)."),
|
|
22
|
+
}, async (a) => {
|
|
23
|
+
const owner = await client.resolveOwner();
|
|
24
|
+
const projectId = a.project_id ?? owner.projectId;
|
|
25
|
+
const workspaceId = a.workspace_id ?? owner.workspaceId;
|
|
26
|
+
const body = {
|
|
27
|
+
...(owner.ownerType ? { ownerType: owner.ownerType } : {}),
|
|
28
|
+
...(owner.ownerId ? { ownerId: owner.ownerId } : {}),
|
|
29
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
30
|
+
...(projectId ? { projectId } : {}),
|
|
31
|
+
...(a.name ? { name: a.name } : {}),
|
|
32
|
+
...(a.cpu_cores ? { cpuCores: a.cpu_cores } : {}),
|
|
33
|
+
...(a.memory_mb ? { memoryMb: a.memory_mb } : {}),
|
|
34
|
+
...(a.disk_size_gb ? { diskSizeGb: a.disk_size_gb } : {}),
|
|
35
|
+
...(a.max_duration_seconds ? { maxDuration: `${a.max_duration_seconds}s` } : {}),
|
|
36
|
+
...(a.idle_timeout_minutes ? { idleTimeoutMinutes: a.idle_timeout_minutes } : {}),
|
|
37
|
+
...(a.clone_repo_url ? { cloneRepoUrl: a.clone_repo_url } : {}),
|
|
38
|
+
...(a.allow_outbound ? { allowOutbound: true } : {}),
|
|
39
|
+
...(a.allow_inbound ? { allowInbound: true } : {}),
|
|
40
|
+
...(a.snapshot_id ? { snapshotId: a.snapshot_id } : {}),
|
|
41
|
+
...(a.registry_ref ? { registryRef: a.registry_ref } : {}),
|
|
42
|
+
...(a.tags && a.tags.length ? { tags: a.tags } : {}),
|
|
43
|
+
...(a.env && Object.keys(a.env).length ? { env: a.env } : {}),
|
|
44
|
+
};
|
|
45
|
+
const resp = await client.control("CreateSession", body);
|
|
46
|
+
const session = resp.session ?? resp;
|
|
47
|
+
const sessionId = session.id ?? resp.sessionId;
|
|
48
|
+
const dataPlaneEndpoint = resp.dataPlaneEndpoint ?? resp.data_plane_endpoint;
|
|
49
|
+
const wait = a.wait_ready !== false;
|
|
50
|
+
const finalSession = wait && sessionId ? await client.waitForState(sessionId, "RUNNING") : session;
|
|
51
|
+
return ok({ session: finalSession, dataPlaneEndpoint });
|
|
52
|
+
});
|
|
53
|
+
server.tool("tenki_get_sandbox", "Fetch a sandbox's current state and metadata.", { session_id: sessionIdSchema }, async ({ session_id }) => ok(await client.control("GetSession", { sessionId: session_id })));
|
|
54
|
+
server.tool("tenki_list_sandboxes", "List sandboxes for the workspace.", {
|
|
55
|
+
include_terminated: z.boolean().optional().describe("Include terminated sandboxes (default false)."),
|
|
56
|
+
page_size: z.number().int().positive().optional(),
|
|
57
|
+
page_token: z.string().optional(),
|
|
58
|
+
}, async ({ include_terminated, page_size, page_token }) => ok(await client.control("ListSessions", {
|
|
59
|
+
...(include_terminated ? { includeTerminated: true } : {}),
|
|
60
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
61
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
62
|
+
})));
|
|
63
|
+
server.tool("tenki_terminate_sandbox", "Terminate (destroy) a sandbox. The microVM and its filesystem are gone after this.", { session_id: sessionIdSchema }, async ({ session_id }) => ok(await client.control("TerminateSession", { sessionId: session_id })));
|
|
64
|
+
server.tool("tenki_pause_sandbox", "Pause a sandbox (snapshot + suspend) so it can be resumed later.", { session_id: sessionIdSchema }, async ({ session_id }) => ok(await client.control("PauseSession", { sessionId: session_id })));
|
|
65
|
+
server.tool("tenki_resume_sandbox", "Resume a previously paused sandbox.", { session_id: sessionIdSchema }, async ({ session_id }) => ok(await client.control("ResumeSession", { sessionId: session_id })));
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { TenkiClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extended sandbox (session) admin ops on the control plane: wall-clock lifetime
|
|
5
|
+
* extension, mutable-field updates, bulk termination, activity heartbeats, and
|
|
6
|
+
* workspace/project-scoped listing. The lifecycle basics
|
|
7
|
+
* (create/get/list/terminate/pause/resume) live in sandboxes.ts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerSessionsAdmin(server: McpServer, client: TenkiClient): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, sessionIdSchema } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extended sandbox (session) admin ops on the control plane: wall-clock lifetime
|
|
5
|
+
* extension, mutable-field updates, bulk termination, activity heartbeats, and
|
|
6
|
+
* workspace/project-scoped listing. The lifecycle basics
|
|
7
|
+
* (create/get/list/terminate/pause/resume) live in sandboxes.ts.
|
|
8
|
+
*/
|
|
9
|
+
export function registerSessionsAdmin(server, client) {
|
|
10
|
+
server.tool("tenki_extend_sandbox", "Extend a running sandbox's wall-clock lifetime by N seconds so it isn't auto-terminated at its max-duration cap.", {
|
|
11
|
+
session_id: sessionIdSchema.describe("The sandbox/session ID to extend."),
|
|
12
|
+
additional_duration_seconds: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.positive()
|
|
16
|
+
.describe("Extra lifetime to add, in seconds (sent as a Duration string, e.g. 3600s)."),
|
|
17
|
+
}, async ({ session_id, additional_duration_seconds }) => ok(await client.control("ExtendSession", {
|
|
18
|
+
sessionId: session_id,
|
|
19
|
+
additionalDuration: `${additional_duration_seconds}s`,
|
|
20
|
+
})));
|
|
21
|
+
server.tool("tenki_update_sandbox", "Update mutable fields on an existing sandbox — its name, tags, idle timeout, or max duration.", {
|
|
22
|
+
session_id: sessionIdSchema.describe("The sandbox/session ID to update."),
|
|
23
|
+
name: z.string().optional().describe("New human-readable name."),
|
|
24
|
+
tags: z.array(z.string()).optional().describe("Replacement tag list (send [] to clear all tags)."),
|
|
25
|
+
idle_timeout_minutes: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Auto-pause after this many idle minutes (cost-safety cap)."),
|
|
31
|
+
max_duration_seconds: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("New hard lifetime cap in seconds (sent as a Duration string, e.g. 3600s)."),
|
|
37
|
+
}, async ({ session_id, name, tags, idle_timeout_minutes, max_duration_seconds }) => ok(await client.control("UpdateSession", {
|
|
38
|
+
sessionId: session_id,
|
|
39
|
+
...(name !== undefined ? { name } : {}),
|
|
40
|
+
...(tags !== undefined ? { tags } : {}),
|
|
41
|
+
...(idle_timeout_minutes !== undefined ? { idleTimeoutMinutes: idle_timeout_minutes } : {}),
|
|
42
|
+
...(max_duration_seconds !== undefined ? { maxDuration: `${max_duration_seconds}s` } : {}),
|
|
43
|
+
})));
|
|
44
|
+
server.tool("tenki_terminate_sandboxes", "Terminate MULTIPLE sandboxes in one call (bulk). IRREVERSIBLE — every listed sandbox and its filesystem is destroyed. Use tenki_terminate_sandbox for a single one.", {
|
|
45
|
+
session_ids: z.array(sessionIdSchema).min(1).describe("The sandbox/session IDs to terminate."),
|
|
46
|
+
}, async ({ session_ids }) => ok(await client.control("TerminateSessions", { sessionIds: session_ids })));
|
|
47
|
+
server.tool("tenki_report_sandbox_activity", "Report client-side activity on a sandbox to reset its idle timer and keep it from being reaped as idle (a keep-alive heartbeat).", { session_id: sessionIdSchema.describe("The sandbox/session ID to mark as active.") }, async ({ session_id }) => ok(await client.control("ReportSessionActivity", { sessionId: session_id })));
|
|
48
|
+
server.tool("tenki_list_workspace_sandboxes", "List every sandbox belonging to a specific workspace (defaults to the API key's workspace) — useful for spotting leaked, still-billing sandboxes across the workspace.", {
|
|
49
|
+
workspace_id: z.string().optional().describe("Workspace to list (defaults to the key's first workspace)."),
|
|
50
|
+
include_terminated: z.boolean().optional().describe("Include terminated sandboxes (default false)."),
|
|
51
|
+
page_size: z.number().int().positive().optional(),
|
|
52
|
+
page_token: z.string().optional(),
|
|
53
|
+
}, async ({ workspace_id, include_terminated, page_size, page_token }) => {
|
|
54
|
+
const workspaceId = workspace_id ?? (await client.resolveOwner()).workspaceId;
|
|
55
|
+
return ok(await client.control("ListWorkspaceSandboxes", {
|
|
56
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
57
|
+
...(include_terminated ? { includeTerminated: true } : {}),
|
|
58
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
59
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
server.tool("tenki_list_project_sandboxes", "List every sandbox belonging to a specific project (defaults to the API key's default project).", {
|
|
63
|
+
project_id: z.string().optional().describe("Project to list (defaults to the key's first project)."),
|
|
64
|
+
include_terminated: z.boolean().optional().describe("Include terminated sandboxes (default false)."),
|
|
65
|
+
page_size: z.number().int().positive().optional(),
|
|
66
|
+
page_token: z.string().optional(),
|
|
67
|
+
}, async ({ project_id, include_terminated, page_size, page_token }) => {
|
|
68
|
+
const projectId = project_id ?? (await client.resolveOwner()).projectId;
|
|
69
|
+
return ok(await client.control("ListProjectSandboxes", {
|
|
70
|
+
...(projectId ? { projectId } : {}),
|
|
71
|
+
...(include_terminated ? { includeTerminated: true } : {}),
|
|
72
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
73
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { TenkiClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Snapshots — capture a sandbox's disk + memory as a reusable image.
|
|
5
|
+
*
|
|
6
|
+
* Restoring is deliberately NOT a tool here: booting a fresh sandbox from a
|
|
7
|
+
* snapshot is `tenki_create_sandbox` with `snapshot_id` (CreateSession under the
|
|
8
|
+
* hood). Attached volumes are NOT captured in a snapshot and must be re-attached
|
|
9
|
+
* to the restored session separately.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerSnapshots(server: McpServer, client: TenkiClient): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, sessionIdSchema } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Snapshots — capture a sandbox's disk + memory as a reusable image.
|
|
5
|
+
*
|
|
6
|
+
* Restoring is deliberately NOT a tool here: booting a fresh sandbox from a
|
|
7
|
+
* snapshot is `tenki_create_sandbox` with `snapshot_id` (CreateSession under the
|
|
8
|
+
* hood). Attached volumes are NOT captured in a snapshot and must be re-attached
|
|
9
|
+
* to the restored session separately.
|
|
10
|
+
*/
|
|
11
|
+
export function registerSnapshots(server, client) {
|
|
12
|
+
server.tool("tenki_create_snapshot", "Capture a running sandbox's disk and memory as a reusable snapshot (attached volumes are NOT captured); boot a new sandbox from it later with tenki_create_sandbox + snapshot_id.", {
|
|
13
|
+
session_id: sessionIdSchema.describe("The sandbox session to snapshot."),
|
|
14
|
+
name: z.string().optional().describe("Human-readable name for the snapshot."),
|
|
15
|
+
expires_at: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("RFC-3339 / ISO-8601 timestamp at which the snapshot is auto-deleted. Omit to keep indefinitely."),
|
|
19
|
+
store_raw_image: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Also store the raw disk image alongside the snapshot (needed to download it later)."),
|
|
23
|
+
}, async ({ session_id, name, expires_at, store_raw_image }) => ok(await client.control("CreateSnapshot", {
|
|
24
|
+
sessionId: session_id,
|
|
25
|
+
...(name ? { name } : {}),
|
|
26
|
+
...(expires_at ? { expiresAt: expires_at } : {}),
|
|
27
|
+
...(store_raw_image !== undefined ? { storeRawImage: store_raw_image } : {}),
|
|
28
|
+
})));
|
|
29
|
+
server.tool("tenki_get_snapshot", "Fetch one snapshot's status and metadata by ID.", { snapshot_id: z.string() }, async ({ snapshot_id }) => ok(await client.control("GetSnapshot", { snapshotId: snapshot_id })));
|
|
30
|
+
server.tool("tenki_list_snapshots", "List the saved snapshots for the workspace (owner inferred from the API key).", {
|
|
31
|
+
page_size: z.number().int().positive().optional(),
|
|
32
|
+
page_token: z.string().optional(),
|
|
33
|
+
}, async ({ page_size, page_token }) => ok(await client.control("ListSnapshots", {
|
|
34
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
35
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
36
|
+
})));
|
|
37
|
+
server.tool("tenki_list_session_snapshots", "List the snapshots captured from a specific sandbox session.", {
|
|
38
|
+
session_id: sessionIdSchema,
|
|
39
|
+
page_size: z.number().int().positive().optional(),
|
|
40
|
+
page_token: z.string().optional(),
|
|
41
|
+
}, async ({ session_id, page_size, page_token }) => ok(await client.control("ListSessionSnapshots", {
|
|
42
|
+
sessionId: session_id,
|
|
43
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
44
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
45
|
+
})));
|
|
46
|
+
server.tool("tenki_list_dangling_snapshots", "List dangling snapshots — those whose source session no longer exists — for the workspace, useful for cleanup.", {
|
|
47
|
+
page_size: z.number().int().positive().optional(),
|
|
48
|
+
page_token: z.string().optional(),
|
|
49
|
+
}, async ({ page_size, page_token }) => ok(await client.control("ListDanglingSnapshots", {
|
|
50
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
51
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
52
|
+
})));
|
|
53
|
+
server.tool("tenki_update_snapshot", "Update a snapshot's mutable metadata (name and/or expiry).", {
|
|
54
|
+
snapshot_id: z.string(),
|
|
55
|
+
name: z.string().optional().describe("New human-readable name."),
|
|
56
|
+
expires_at: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("New RFC-3339 / ISO-8601 auto-delete timestamp."),
|
|
60
|
+
}, async ({ snapshot_id, name, expires_at }) => ok(await client.control("UpdateSnapshot", {
|
|
61
|
+
snapshotId: snapshot_id,
|
|
62
|
+
...(name !== undefined ? { name } : {}),
|
|
63
|
+
...(expires_at !== undefined ? { expiresAt: expires_at } : {}),
|
|
64
|
+
})));
|
|
65
|
+
server.tool("tenki_delete_snapshot", "Permanently delete a snapshot by ID.", { snapshot_id: z.string() }, async ({ snapshot_id }) => ok(await client.control("DeleteSnapshot", { snapshotId: snapshot_id })));
|
|
66
|
+
server.tool("tenki_get_snapshot_download_url", "Get a short-lived, pre-signed URL to download a snapshot's raw disk image (requires the snapshot to have been created with store_raw_image).", { snapshot_id: z.string() }, async ({ snapshot_id }) => ok(await client.control("GetSnapshotDownloadURL", { snapshotId: snapshot_id })));
|
|
67
|
+
server.tool("tenki_list_workspace_snapshots", "List all snapshots in a workspace (defaults to the key's first workspace). Supports pagination.", {
|
|
68
|
+
workspace_id: z.string().optional().describe("Workspace to list (defaults to the key's first workspace)."),
|
|
69
|
+
page_size: z.number().int().positive().optional(),
|
|
70
|
+
page_token: z.string().optional(),
|
|
71
|
+
}, async ({ workspace_id, page_size, page_token }) => {
|
|
72
|
+
const workspaceId = workspace_id ?? (await client.resolveOwner()).workspaceId;
|
|
73
|
+
return ok(await client.control("ListWorkspaceSnapshots", {
|
|
74
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
75
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
76
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
server.tool("tenki_list_project_snapshots", "List all snapshots in a project (defaults to the key's first project). Supports pagination.", {
|
|
80
|
+
project_id: z.string().optional().describe("Project to list (defaults to the key's first project)."),
|
|
81
|
+
page_size: z.number().int().positive().optional(),
|
|
82
|
+
page_token: z.string().optional(),
|
|
83
|
+
}, async ({ project_id, page_size, page_token }) => {
|
|
84
|
+
const projectId = project_id ?? (await client.resolveOwner()).projectId;
|
|
85
|
+
return ok(await client.control("ListProjectSnapshots", {
|
|
86
|
+
...(projectId ? { projectId } : {}),
|
|
87
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
88
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ssh.ts — SSH access tools for tenki-mcp.
|
|
3
|
+
*
|
|
4
|
+
* UpdateSSHAuthorizedKeys sets the authorized_keys on a running sandbox (on
|
|
5
|
+
* SandboxService). IssueSandboxSSHCert and ListActiveSSHGateways live on a
|
|
6
|
+
* SEPARATE ConnectRPC service (SSHGatewayClientService), reached by passing the
|
|
7
|
+
* service path to client.control.
|
|
8
|
+
*
|
|
9
|
+
* Request shapes are grounded in the published Tenki API surface (CreateSession's
|
|
10
|
+
* ssh_authorized_keys field → sshAuthorizedKeys). The cert-issuance and gateway
|
|
11
|
+
* shapes are SDK-name-verified but not exercised end-to-end here.
|
|
12
|
+
*/
|
|
13
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import type { TenkiClient } from "../client.js";
|
|
15
|
+
export declare function registerSsh(server: McpServer, client: TenkiClient): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, sessionIdSchema } from "./common.js";
|
|
3
|
+
const SSH_GATEWAY_SERVICE = "tenki.sandbox.v1.SSHGatewayClientService";
|
|
4
|
+
export function registerSsh(server, client) {
|
|
5
|
+
server.tool("tenki_update_ssh_keys", "Set the SSH authorized public keys on a running sandbox, enabling direct SSH access for the given keys.", {
|
|
6
|
+
session_id: sessionIdSchema,
|
|
7
|
+
public_keys: z.array(z.string()).describe("SSH public keys (ssh-ed25519 …, ssh-rsa …) to authorize. Replaces the current set."),
|
|
8
|
+
}, async ({ session_id, public_keys }) => ok(await client.control("UpdateSSHAuthorizedKeys", { sessionId: session_id, sshAuthorizedKeys: public_keys })));
|
|
9
|
+
server.tool("tenki_issue_ssh_cert", "Issue a short-lived SSH certificate for a public key, authorizing SSH access to a sandbox via the SSH gateway.", {
|
|
10
|
+
session_id: sessionIdSchema,
|
|
11
|
+
public_key: z.string().describe("The SSH public key to sign into a certificate."),
|
|
12
|
+
}, async ({ session_id, public_key }) => ok(await client.control("IssueSandboxSSHCert", { sessionId: session_id, publicKey: public_key }, SSH_GATEWAY_SERVICE)));
|
|
13
|
+
server.tool("tenki_list_ssh_gateways", "List the currently active SSH gateways for the workspace.", { workspace_id: z.string().optional().describe("Workspace to list (defaults to the key's first workspace).") }, async ({ workspace_id }) => {
|
|
14
|
+
const workspaceId = workspace_id ?? (await client.resolveOwner()).workspaceId;
|
|
15
|
+
return ok(await client.control("ListActiveSSHGateways", { ...(workspaceId ? { workspaceId } : {}) }, SSH_GATEWAY_SERVICE));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { TenkiClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Template tools — custom-image templates and their builds.
|
|
5
|
+
*
|
|
6
|
+
* A "template" is a reusable sandbox-image spec (base image + setup script +
|
|
7
|
+
* default resources/env); building one produces a snapshot/image that sandboxes
|
|
8
|
+
* can boot from. Method names and request fields are matched to the generated
|
|
9
|
+
* `tenki.sandbox.v1.SandboxService` protobuf (the wire contract the control
|
|
10
|
+
* plane actually speaks). Notable shapes: sizing is a nested `resources` object
|
|
11
|
+
* ({ cpuCores, memoryMb, diskSizeGb }); the env map is `envVars`; a build is
|
|
12
|
+
* addressed by `buildId`; and ListActiveTemplateBuilds is scoped by `templateId`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerTemplates(server: McpServer, client: TenkiClient): void;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, envSchema } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Template tools — custom-image templates and their builds.
|
|
5
|
+
*
|
|
6
|
+
* A "template" is a reusable sandbox-image spec (base image + setup script +
|
|
7
|
+
* default resources/env); building one produces a snapshot/image that sandboxes
|
|
8
|
+
* can boot from. Method names and request fields are matched to the generated
|
|
9
|
+
* `tenki.sandbox.v1.SandboxService` protobuf (the wire contract the control
|
|
10
|
+
* plane actually speaks). Notable shapes: sizing is a nested `resources` object
|
|
11
|
+
* ({ cpuCores, memoryMb, diskSizeGb }); the env map is `envVars`; a build is
|
|
12
|
+
* addressed by `buildId`; and ListActiveTemplateBuilds is scoped by `templateId`.
|
|
13
|
+
*/
|
|
14
|
+
export function registerTemplates(server, client) {
|
|
15
|
+
/** Assemble the nested TemplateResources object from flat sizing params (omitting any unset). */
|
|
16
|
+
const resourcesFrom = (cpuCores, memoryMb, diskSizeGb) => {
|
|
17
|
+
const r = {};
|
|
18
|
+
if (cpuCores !== undefined)
|
|
19
|
+
r.cpuCores = cpuCores;
|
|
20
|
+
if (memoryMb !== undefined)
|
|
21
|
+
r.memoryMb = memoryMb;
|
|
22
|
+
if (diskSizeGb !== undefined)
|
|
23
|
+
r.diskSizeGb = diskSizeGb;
|
|
24
|
+
return r;
|
|
25
|
+
};
|
|
26
|
+
// ── Create ──────────────────────────────────────────────────────────────────
|
|
27
|
+
server.tool("tenki_create_template", "Create a custom-image template (a reusable sandbox-image spec: base image + setup script + default resources). Build it into a bootable image later with tenki_build_template.", {
|
|
28
|
+
name: z.string().describe("Human-readable template name."),
|
|
29
|
+
base_image_id: z.string().optional().describe("Base image ID to build on top of."),
|
|
30
|
+
setup_script: z.string().optional().describe("Shell script run at build time to provision the image. Required for a from-scratch template (the API rejects a create without it unless you derive from a parent template/image)."),
|
|
31
|
+
start_cmd: z.string().optional().describe("Command run when a sandbox boots from this template."),
|
|
32
|
+
cpu_cores: z.number().int().min(1).max(16).optional().describe("Default vCPUs for sandboxes from this template (1-16)."),
|
|
33
|
+
memory_mb: z.number().int().min(512).max(65536).optional().describe("Default memory in MB (512-65536)."),
|
|
34
|
+
disk_size_gb: z.number().int().min(5).max(100).optional().describe("Default disk in GB (5-100)."),
|
|
35
|
+
env_vars: envSchema,
|
|
36
|
+
tags: z.array(z.string()).optional().describe("Tags for later filtering."),
|
|
37
|
+
parent_template_id: z.string().optional().describe("Derive this template from an existing template."),
|
|
38
|
+
parent_image: z.string().optional().describe("Derive this template from an existing built image reference."),
|
|
39
|
+
builder_spec: z.record(z.unknown()).optional().describe("Advanced structured build spec (TemplateBuildSpec); passed through as-is."),
|
|
40
|
+
workspace_id: z.string().optional().describe("Workspace to create in (defaults to the key's first workspace)."),
|
|
41
|
+
project_id: z.string().optional().describe("Project to create in (defaults to the key's first project)."),
|
|
42
|
+
}, async (a) => {
|
|
43
|
+
const owner = await client.resolveOwner();
|
|
44
|
+
const workspaceId = a.workspace_id ?? owner.workspaceId;
|
|
45
|
+
const projectId = a.project_id ?? owner.projectId;
|
|
46
|
+
const resources = resourcesFrom(a.cpu_cores, a.memory_mb, a.disk_size_gb);
|
|
47
|
+
const body = {
|
|
48
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
49
|
+
name: a.name,
|
|
50
|
+
...(a.base_image_id ? { baseImageId: a.base_image_id } : {}),
|
|
51
|
+
...(a.setup_script !== undefined ? { setupScript: a.setup_script } : {}),
|
|
52
|
+
...(a.start_cmd !== undefined ? { startCmd: a.start_cmd } : {}),
|
|
53
|
+
...(a.env_vars && Object.keys(a.env_vars).length ? { envVars: a.env_vars } : {}),
|
|
54
|
+
...(Object.keys(resources).length ? { resources } : {}),
|
|
55
|
+
...(projectId ? { projectId } : {}),
|
|
56
|
+
...(a.tags && a.tags.length ? { tags: a.tags } : {}),
|
|
57
|
+
...(a.parent_template_id ? { parentTemplateId: a.parent_template_id } : {}),
|
|
58
|
+
...(a.parent_image ? { parentImage: a.parent_image } : {}),
|
|
59
|
+
...(a.builder_spec ? { builderSpec: a.builder_spec } : {}),
|
|
60
|
+
};
|
|
61
|
+
return ok(await client.control("CreateTemplate", body));
|
|
62
|
+
});
|
|
63
|
+
// ── Get ─────────────────────────────────────────────────────────────────────
|
|
64
|
+
server.tool("tenki_get_template", "Retrieve one template by ID.", { template_id: z.string().describe("The template ID.") }, async ({ template_id }) => ok(await client.control("GetTemplate", { templateId: template_id })));
|
|
65
|
+
// ── List ────────────────────────────────────────────────────────────────────
|
|
66
|
+
server.tool("tenki_list_templates", "List templates for the workspace, optionally filtered by tags.", {
|
|
67
|
+
tags: z.array(z.string()).optional().describe("Only return templates that carry all of these tags."),
|
|
68
|
+
workspace_id: z.string().optional().describe("Workspace to list from (defaults to the key's first workspace)."),
|
|
69
|
+
page_size: z.number().int().positive().optional(),
|
|
70
|
+
page_token: z.string().optional(),
|
|
71
|
+
}, async ({ tags, workspace_id, page_size, page_token }) => {
|
|
72
|
+
const owner = await client.resolveOwner();
|
|
73
|
+
const workspaceId = workspace_id ?? owner.workspaceId;
|
|
74
|
+
return ok(await client.control("ListTemplates", {
|
|
75
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
76
|
+
...(tags && tags.length ? { tags } : {}),
|
|
77
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
78
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
// ── Update ──────────────────────────────────────────────────────────────────
|
|
82
|
+
server.tool("tenki_update_template", "Update mutable fields on a template. Only the fields you provide are changed; pass clear_tags to remove all tags.", {
|
|
83
|
+
template_id: z.string().describe("The template ID to update."),
|
|
84
|
+
name: z.string().optional().describe("New human-readable name."),
|
|
85
|
+
base_image_id: z.string().optional().describe("New base image ID."),
|
|
86
|
+
setup_script: z.string().optional().describe("New build-time provisioning script."),
|
|
87
|
+
start_cmd: z.string().optional().describe("New boot command."),
|
|
88
|
+
cpu_cores: z.number().int().min(1).max(16).optional().describe("New default vCPUs (1-16)."),
|
|
89
|
+
memory_mb: z.number().int().min(512).max(65536).optional().describe("New default memory in MB (512-65536)."),
|
|
90
|
+
disk_size_gb: z.number().int().min(5).max(100).optional().describe("New default disk in GB (5-100)."),
|
|
91
|
+
env_vars: envSchema,
|
|
92
|
+
tags: z.array(z.string()).optional().describe("Replacement set of tags."),
|
|
93
|
+
clear_tags: z.boolean().optional().describe("Remove all tags from the template."),
|
|
94
|
+
builder_spec: z.record(z.unknown()).optional().describe("Advanced structured build spec (TemplateBuildSpec); passed through as-is."),
|
|
95
|
+
}, async (a) => {
|
|
96
|
+
const resources = resourcesFrom(a.cpu_cores, a.memory_mb, a.disk_size_gb);
|
|
97
|
+
const body = {
|
|
98
|
+
templateId: a.template_id,
|
|
99
|
+
...(a.name !== undefined ? { name: a.name } : {}),
|
|
100
|
+
...(a.base_image_id !== undefined ? { baseImageId: a.base_image_id } : {}),
|
|
101
|
+
...(a.setup_script !== undefined ? { setupScript: a.setup_script } : {}),
|
|
102
|
+
...(a.start_cmd !== undefined ? { startCmd: a.start_cmd } : {}),
|
|
103
|
+
...(a.env_vars && Object.keys(a.env_vars).length ? { envVars: a.env_vars } : {}),
|
|
104
|
+
...(Object.keys(resources).length ? { resources } : {}),
|
|
105
|
+
...(a.tags && a.tags.length ? { tags: a.tags } : {}),
|
|
106
|
+
...(a.clear_tags ? { clearTags: true } : {}),
|
|
107
|
+
...(a.builder_spec ? { builderSpec: a.builder_spec } : {}),
|
|
108
|
+
};
|
|
109
|
+
return ok(await client.control("UpdateTemplate", body));
|
|
110
|
+
});
|
|
111
|
+
// ── Delete ──────────────────────────────────────────────────────────────────
|
|
112
|
+
server.tool("tenki_delete_template", "Delete a template by ID. Pass force to delete even when builds or dependents exist.", {
|
|
113
|
+
template_id: z.string().describe("The template ID to delete."),
|
|
114
|
+
force: z.boolean().optional().describe("Force deletion despite dependents (default false)."),
|
|
115
|
+
}, async ({ template_id, force }) => ok(await client.control("DeleteTemplate", {
|
|
116
|
+
templateId: template_id,
|
|
117
|
+
...(force ? { force: true } : {}),
|
|
118
|
+
})));
|
|
119
|
+
// ── Build ───────────────────────────────────────────────────────────────────
|
|
120
|
+
server.tool("tenki_build_template", "Trigger a build for a template, producing a bootable image. Returns the created build (poll it with tenki_get_template_build).", {
|
|
121
|
+
template_id: z.string().describe("The template ID to build."),
|
|
122
|
+
image_name: z.string().optional().describe("Name for the resulting image."),
|
|
123
|
+
publish_raw_image: z.boolean().optional().describe("Publish the raw rootfs image alongside the build snapshot."),
|
|
124
|
+
build_secrets: z.record(z.string()).optional().describe("Build-time secrets as a key→value object (not persisted into the image)."),
|
|
125
|
+
build_env: z.record(z.string()).optional().describe("Per-build environment overrides frozen into this build only."),
|
|
126
|
+
}, async ({ template_id, image_name, publish_raw_image, build_secrets, build_env }) => ok(await client.control("BuildTemplate", {
|
|
127
|
+
templateId: template_id,
|
|
128
|
+
...(image_name !== undefined ? { imageName: image_name } : {}),
|
|
129
|
+
...(publish_raw_image !== undefined ? { publishRawImage: publish_raw_image } : {}),
|
|
130
|
+
...(build_secrets && Object.keys(build_secrets).length ? { buildSecrets: build_secrets } : {}),
|
|
131
|
+
...(build_env && Object.keys(build_env).length ? { buildEnv: build_env } : {}),
|
|
132
|
+
})));
|
|
133
|
+
// ── Cancel build ──────────────────────────────────────────────────────────────
|
|
134
|
+
server.tool("tenki_cancel_template_build", "Cancel an in-progress template build by its build ID.", { build_id: z.string().describe("The template build ID to cancel.") }, async ({ build_id }) => ok(await client.control("CancelTemplateBuild", { buildId: build_id })));
|
|
135
|
+
// ── Get build ─────────────────────────────────────────────────────────────────
|
|
136
|
+
server.tool("tenki_get_template_build", "Retrieve one template build by its build ID (state, progress, and result image).", { build_id: z.string().describe("The template build ID.") }, async ({ build_id }) => ok(await client.control("GetTemplateBuild", { buildId: build_id })));
|
|
137
|
+
// ── List active builds ──────────────────────────────────────────────────────────
|
|
138
|
+
server.tool("tenki_list_active_template_builds", "List the currently active (in-progress) builds for a given template.", { template_id: z.string().describe("The template ID whose active builds to list.") }, async ({ template_id }) => ok(await client.control("ListActiveTemplateBuilds", { templateId: template_id })));
|
|
139
|
+
server.tool("tenki_list_project_templates", "List templates in a project (defaults to the key's first project). Supports pagination.", {
|
|
140
|
+
project_id: z.string().optional().describe("Project to list (defaults to the key's first project)."),
|
|
141
|
+
page_size: z.number().int().positive().optional(),
|
|
142
|
+
page_token: z.string().optional(),
|
|
143
|
+
}, async ({ project_id, page_size, page_token }) => {
|
|
144
|
+
const projectId = project_id ?? (await client.resolveOwner()).projectId;
|
|
145
|
+
return ok(await client.control("ListProjectTemplates", {
|
|
146
|
+
...(projectId ? { projectId } : {}),
|
|
147
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
148
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
149
|
+
}));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volume tools — workspace-scoped persistent block storage for Tenki sandboxes.
|
|
3
|
+
*
|
|
4
|
+
* Volumes are durable disks that outlive any single sandbox: create one in a
|
|
5
|
+
* workspace, then attach it into a running session at a mount path. All calls
|
|
6
|
+
* here are control-plane (tenki.sandbox.v1.SandboxService). Method names and
|
|
7
|
+
* request field shapes are ported from the live-verified n8n community node
|
|
8
|
+
* (github.com/opencolin/n8n-nodes-tenki) and its endpoint research:
|
|
9
|
+
* CreateVolume · GetVolume · ListVolumes · UpdateVolume · DeleteVolume ·
|
|
10
|
+
* ResizeVolume · AttachVolume · DetachVolume
|
|
11
|
+
*
|
|
12
|
+
* CreateVolumeRequest is flat: { workspaceId, name, sizeBytes (int64), projectId? }.
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
import type { TenkiClient } from "../client.js";
|
|
16
|
+
export declare function registerVolumes(server: McpServer, client: TenkiClient): void;
|