@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,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok, pathSchema, sessionIdSchema } from "./common.js";
|
|
3
|
+
/** Volume size bounds the control plane accepts: 1 MiB … 100 GiB, in bytes. */
|
|
4
|
+
const MIN_VOLUME_BYTES = 1_048_576; // 1 MiB
|
|
5
|
+
const MAX_VOLUME_BYTES = 107_374_182_400; // 100 GiB
|
|
6
|
+
/** Shared size-in-bytes schema for create + resize, range-checked before the call. */
|
|
7
|
+
const sizeBytesSchema = z
|
|
8
|
+
.number()
|
|
9
|
+
.int()
|
|
10
|
+
.min(MIN_VOLUME_BYTES)
|
|
11
|
+
.max(MAX_VOLUME_BYTES)
|
|
12
|
+
.describe("Volume size in bytes. Must be between 1 MiB (1048576) and 100 GiB (107374182400).");
|
|
13
|
+
export function registerVolumes(server, client) {
|
|
14
|
+
// ── Create ────────────────────────────────────────────────────────────────
|
|
15
|
+
server.tool("tenki_create_volume", "Create a workspace-scoped persistent volume — durable block storage that survives sandbox teardown. Defaults the workspace and project to the API key's first; override with workspace_id/project_id.", {
|
|
16
|
+
name: z.string().describe("Human-readable name for the volume."),
|
|
17
|
+
size_bytes: sizeBytesSchema,
|
|
18
|
+
workspace_id: z.string().optional().describe("Workspace to create the volume in (defaults to the key's first workspace)."),
|
|
19
|
+
project_id: z.string().optional().describe("Project to associate the volume with (defaults to the key's first project)."),
|
|
20
|
+
}, async ({ name, size_bytes, workspace_id, project_id }) => {
|
|
21
|
+
const owner = await client.resolveOwner();
|
|
22
|
+
const workspaceId = workspace_id ?? owner.workspaceId;
|
|
23
|
+
const projectId = project_id ?? owner.projectId;
|
|
24
|
+
return ok(await client.control("CreateVolume", {
|
|
25
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
26
|
+
name,
|
|
27
|
+
sizeBytes: size_bytes,
|
|
28
|
+
...(projectId ? { projectId } : {}),
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
// ── Get ───────────────────────────────────────────────────────────────────
|
|
32
|
+
server.tool("tenki_get_volume", "Fetch a single volume's metadata and current state by its id.", { volume_id: z.string().describe("The volume id, e.g. vol_….") }, async ({ volume_id }) => ok(await client.control("GetVolume", { volumeId: volume_id })));
|
|
33
|
+
// ── List ──────────────────────────────────────────────────────────────────
|
|
34
|
+
server.tool("tenki_list_volumes", "List persistent volumes in a workspace (defaults to the key's first workspace). Supports pagination.", {
|
|
35
|
+
workspace_id: z.string().optional().describe("Workspace to list volumes from (defaults to the key's first workspace)."),
|
|
36
|
+
page_size: z.number().int().positive().optional().describe("Max volumes to return per page."),
|
|
37
|
+
page_token: z.string().optional().describe("Page token from a previous response's nextPageToken."),
|
|
38
|
+
}, async ({ workspace_id, page_size, page_token }) => {
|
|
39
|
+
const owner = await client.resolveOwner();
|
|
40
|
+
const workspaceId = workspace_id ?? owner.workspaceId;
|
|
41
|
+
return ok(await client.control("ListVolumes", {
|
|
42
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
43
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
44
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
// ── Update ────────────────────────────────────────────────────────────────
|
|
48
|
+
server.tool("tenki_update_volume", "Rename a volume (update its human-readable name). To change a volume's size use tenki_resize_volume instead.", {
|
|
49
|
+
volume_id: z.string().describe("The volume id to update."),
|
|
50
|
+
name: z.string().describe("New human-readable name for the volume."),
|
|
51
|
+
}, async ({ volume_id, name }) => ok(await client.control("UpdateVolume", { volumeId: volume_id, name })));
|
|
52
|
+
// ── Delete ────────────────────────────────────────────────────────────────
|
|
53
|
+
server.tool("tenki_delete_volume", "Permanently delete a volume and destroy its data. Fails with VolumeInUse if the volume is still attached to a session — detach it first.", { volume_id: z.string().describe("The volume id to delete.") }, async ({ volume_id }) => ok(await client.control("DeleteVolume", { volumeId: volume_id })));
|
|
54
|
+
// ── Resize ────────────────────────────────────────────────────────────────
|
|
55
|
+
server.tool("tenki_resize_volume", "Grow a volume to a new size in bytes (1 MiB … 100 GiB). Volumes can grow but not shrink.", {
|
|
56
|
+
volume_id: z.string().describe("The volume id to resize."),
|
|
57
|
+
size_bytes: sizeBytesSchema,
|
|
58
|
+
}, async ({ volume_id, size_bytes }) => ok(await client.control("ResizeVolume", { volumeId: volume_id, newSizeBytes: size_bytes })));
|
|
59
|
+
// ── Attach ────────────────────────────────────────────────────────────────
|
|
60
|
+
server.tool("tenki_attach_volume", "Mount a volume into a running sandbox at an absolute path. Set read_only to mount without write access.", {
|
|
61
|
+
session_id: sessionIdSchema.describe("The sandbox session to attach the volume to."),
|
|
62
|
+
volume_id: z.string().describe("The volume id to attach."),
|
|
63
|
+
mount_path: pathSchema.describe("Absolute path inside the sandbox to mount at, e.g. /mnt/data."),
|
|
64
|
+
read_only: z.boolean().optional().describe("Mount the volume read-only (default false = read-write)."),
|
|
65
|
+
}, async ({ session_id, volume_id, mount_path, read_only }) => ok(
|
|
66
|
+
// AttachVolumeRequest nests the target under a `volume` sub-message
|
|
67
|
+
// (live-verified: a flat volumeId is rejected "volume: value is required").
|
|
68
|
+
await client.control("AttachVolume", {
|
|
69
|
+
sessionId: session_id,
|
|
70
|
+
volume: {
|
|
71
|
+
volumeId: volume_id,
|
|
72
|
+
mountPath: mount_path,
|
|
73
|
+
...(read_only !== undefined ? { readOnly: read_only } : {}),
|
|
74
|
+
},
|
|
75
|
+
})));
|
|
76
|
+
// ── Detach ────────────────────────────────────────────────────────────────
|
|
77
|
+
server.tool("tenki_detach_volume", "Unmount a volume from a sandbox session.", {
|
|
78
|
+
session_id: sessionIdSchema.describe("The sandbox session to detach the volume from."),
|
|
79
|
+
volume_id: z.string().describe("The volume id to detach."),
|
|
80
|
+
}, async ({ session_id, volume_id }) => ok(await client.control("DetachVolume", { sessionId: session_id, volumeId: volume_id })));
|
|
81
|
+
// ── List (project-scoped) ─────────────────────────────────────────────────────
|
|
82
|
+
server.tool("tenki_list_project_volumes", "List persistent volumes in a project (defaults to the key's first project). Supports pagination.", {
|
|
83
|
+
project_id: z.string().optional().describe("Project to list volumes from (defaults to the key's first project)."),
|
|
84
|
+
page_size: z.number().int().positive().optional(),
|
|
85
|
+
page_token: z.string().optional(),
|
|
86
|
+
}, async ({ project_id, page_size, page_token }) => {
|
|
87
|
+
const projectId = project_id ?? (await client.resolveOwner()).projectId;
|
|
88
|
+
return ok(await client.control("ListProjectVolumes", {
|
|
89
|
+
...(projectId ? { projectId } : {}),
|
|
90
|
+
...(page_size ? { pageSize: page_size } : {}),
|
|
91
|
+
...(page_token ? { pageToken: page_token } : {}),
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { TenkiClient } from "../client.js";
|
|
3
|
+
/** Workspace-level sandbox usage + default settings (incl. snapshot retention). */
|
|
4
|
+
export declare function registerWorkspace(server: McpServer, client: TenkiClient): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "./common.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve which workspace an op targets: honour an explicit id, else fall back
|
|
5
|
+
* to the API key's first workspace (via WhoAmI, inside resolveOwner). Mirrors
|
|
6
|
+
* the live-verified n8n node's `resolveWorkspaceId` helper.
|
|
7
|
+
*/
|
|
8
|
+
async function resolveWorkspaceId(client, provided) {
|
|
9
|
+
if (provided && provided.trim())
|
|
10
|
+
return provided.trim();
|
|
11
|
+
const owner = await client.resolveOwner();
|
|
12
|
+
return owner.workspaceId;
|
|
13
|
+
}
|
|
14
|
+
/** Workspace-level sandbox usage + default settings (incl. snapshot retention). */
|
|
15
|
+
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.", {
|
|
17
|
+
workspace_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Workspace to report on. Omit to use the API key's first workspace."),
|
|
21
|
+
}, async ({ workspace_id }) => {
|
|
22
|
+
const workspaceId = await resolveWorkspaceId(client, workspace_id);
|
|
23
|
+
return ok(await client.control("GetWorkspaceSandboxUsage", {
|
|
24
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
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).", {
|
|
28
|
+
workspace_id: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Workspace to read. Omit to use the API key's first workspace."),
|
|
32
|
+
}, async ({ workspace_id }) => {
|
|
33
|
+
const workspaceId = await resolveWorkspaceId(client, workspace_id);
|
|
34
|
+
return ok(await client.control("GetWorkspaceSandboxSettings", {
|
|
35
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
36
|
+
}));
|
|
37
|
+
});
|
|
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.", {
|
|
39
|
+
workspace_id: z
|
|
40
|
+
.string()
|
|
41
|
+
.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 } : {}) }));
|
|
103
|
+
});
|
|
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()
|
|
113
|
+
.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", {
|
|
122
|
+
...(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 } : {}),
|
|
127
|
+
}));
|
|
128
|
+
});
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenkicloud/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.LuxorLabs/tenki-mcp",
|
|
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
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tenki-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"SECURITY.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"pretest": "npm run build",
|
|
24
|
+
"test": "node test/offline.test.mjs && node test/exec-output.test.mjs && node test/http-input.test.mjs && node test/security.test.mjs && node test/client-net.test.mjs",
|
|
25
|
+
"test:all": "npm run build && node test/run.mjs",
|
|
26
|
+
"test:offline": "npm run build && node test/offline.test.mjs"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"tenki",
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"sandbox",
|
|
33
|
+
"microvm",
|
|
34
|
+
"ai-agents",
|
|
35
|
+
"code-execution"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": "Tenki Cloud (https://tenki.cloud)",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
47
|
+
"zod": "^3.23.8"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"typescript": "^5.5.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/LuxorLabs/tenki-mcp.git"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/LuxorLabs/tenki-mcp#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/LuxorLabs/tenki-mcp/issues"
|
|
60
|
+
}
|
|
61
|
+
}
|