@tenkicloud/mcp 0.1.0 → 0.3.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 +30 -145
- package/README.md +35 -10
- package/SECURITY.md +6 -6
- package/dist/client.d.ts +30 -5
- package/dist/client.js +89 -13
- package/dist/http.d.ts +4 -6
- package/dist/http.js +80 -20
- package/dist/index.js +3 -2
- package/dist/oauth.d.ts +33 -0
- package/dist/oauth.js +146 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -4
- package/dist/tools/auth_status.d.ts +2 -2
- package/dist/tools/auth_status.js +9 -4
- package/dist/tools/common.d.ts +3 -1
- package/dist/tools/common.js +20 -2
- package/dist/tools/exec.js +38 -6
- package/dist/tools/git.js +1 -1
- package/dist/tools/previews.d.ts +4 -4
- package/dist/tools/previews.js +15 -37
- package/dist/tools/run.js +1 -1
- package/dist/tools/sandboxes.js +26 -20
- package/dist/tools/sessions_admin.d.ts +1 -1
- package/dist/tools/sessions_admin.js +1 -15
- package/dist/tools/snapshots.js +0 -12
- package/dist/tools/templates.js +14 -23
- package/dist/tools/volumes.d.ts +1 -1
- package/dist/tools/volumes.js +3 -20
- package/package.json +5 -5
- package/dist/tools/registry.d.ts +0 -18
- package/dist/tools/registry.js +0 -98
package/CHANGELOG.md
CHANGED
|
@@ -1,161 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to `@tenkicloud/mcp`. This project follows semantic versioning.
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.2.0] — 2026-08-21
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Remove standalone image management tools. Template images remain available through template builds and sandbox creation.
|
|
8
|
+
- Use `image` when creating a sandbox from a template image; removed image-reference arguments are now rejected.
|
|
9
|
+
- Cap `tenki_exec` / `tenki_run_code` inline output at 64 KB per stream (override with `max_output_bytes` on `tenki_exec`). Larger output returns a head+tail preview with `stdoutTruncated`/`stderrTruncated` set, and the full capture file is kept in the sandbox at the reported `stdoutPath`/`stderrPath`.
|
|
10
|
+
- Remove the project-scoped surface — the `tenki_list_project_sandboxes`/`_templates`/`_volumes`/`_snapshots` tools (their API methods no longer exist) and the `project_id` arguments (ignored by the API). Use the workspace-scoped equivalents.
|
|
11
|
+
- Document the template-image path end to end: `image_name` builds require a typed template (`builder_spec`), and `tenki_create_sandbox`'s `image` takes the ready build's `imageDigestRef`.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## [0.1.0] — 2026-08-03 — Initial release
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
- **`tenki_update_workspace_settings` and `tenki_update_snapshot_retention_settings` did nothing at all, and reported success.** Every field they sent (`defaultIdleTimeoutMinutes`, `defaultMaxDurationSeconds`, `snapshotRetentionDays`, `retentionDays`) is absent from the API; connect-go discards unknown fields, so the server returned 200 with the settings unchanged. They now send the real contract — `pauseRetention`/`snapshotRetention` as protobuf Durations with their `clear*` companions, plus the `max*` quotas — reject a call that would change nothing, and `clear_retention` makes "keep snapshots indefinitely" expressible for the first time. The retention pair is also documented as deprecated upstream in favour of the workspace-settings pair.
|
|
13
|
-
- **`tenki_list_files` hid every dotfile.** The data plane omits dot-prefixed entries unless asked, so `.git`, `.env` and `.gitignore` were invisible and an agent would conclude they did not exist. It now sends `include_hidden` (default true, overridable) and always returns an `entries` array instead of a bare `{}` for an empty directory.
|
|
14
|
-
- **`tenki_list_preview_urls` advertised a filter the API does not have.** `ListPreviewUrls` takes no session id, so the one this tool sent was silently dropped and every session's preview URLs came back as if filtered. The filter is now applied client-side and says so in the result; `page_size`, `page_token` and `workspace_id` — all real fields the tool never exposed — are now available, and the deprecated `project_id` is gone.
|
|
15
|
-
- **`tenki_publish_image`'s documented example could never work:** its own description showed `myws/myimage:latest`, and the API rejects any tag or `@snapshot` on publish. The tagless form is now enforced client-side with an explanatory message.
|
|
16
|
-
- **`tenki_unshare_image` silently revoked nothing** when called with neither `grant_id` nor `grantee_workspace_id`, returning the image object like a success. It now refuses, matching the guard `tenki_delete_image` already had.
|
|
17
|
-
- **`tenki_open_preview` hands back an unreachable URL** for any port but the web terminal (7681): the API deliberately drops the regional host for other ports and returns a fallback that has no edge route, so the URL 404s. The description now says so and points at `tenki_expose_port`/`tenki_create_preview_url`. Platform-side issue; documented rather than worked around.
|
|
15
|
+
Model Context Protocol server for Tenki Cloud — disposable microVM sandboxes for AI agents. Published as **`@tenkicloud/mcp`**, matching the `@tenkicloud/sandbox` SDK. Install with `npx -y @tenkicloud/mcp`; the command it provides is `tenki-mcp`.
|
|
18
16
|
|
|
19
|
-
###
|
|
17
|
+
### Tools
|
|
20
18
|
|
|
21
|
-
- **
|
|
22
|
-
-
|
|
23
|
-
-
|
|
19
|
+
- **85 tools covering the full Tenki unary API**, enforced by a CI parity audit (`scripts/parity-audit.mjs` fails the build if any SandboxService / DataPlane / SSHGateway method lacks a tool): sandbox lifecycle, code execution (`tenki_exec`, `tenki_run_code`), files, git, ports and preview URLs, snapshots, volumes, templates, SSH, artifacts, and workspace administration.
|
|
20
|
+
- **`tenki_git`** validates `operation` as an enum of what the API actually supports (`clone`, `checkout`, `diff`, `log`) with per-operation arg keys documented; other git commands go through `tenki_exec`.
|
|
21
|
+
- **`tenki_exec` returns structured output**: it declares an `outputSchema` and returns `structuredContent` (`stdout`/`stderr`/`exitCode`/`ok`) alongside a human-readable rendering in which control characters, bidi overrides, and zero-width marks are escaped to visible `\xNN`/`\uNNNN` — sandbox output is untrusted.
|
|
22
|
+
- **`tenki_auth_status`** reports whether a usable credential is configured, which kind (`api_key`, `oauth_session_token`, or `session_cookie`), which env var it came from, the endpoint targeted, and whether a live identity probe succeeded — without ever returning the token. The server boots without a credential with this as the only registered tool, so a misconfigured client can ask what is wrong instead of seeing an opaque "server failed to start".
|
|
23
|
+
- Shared, described input schemas across modules: session ids (trimmed, non-empty), ports (1–65535), preview slugs (matching the server's own validation), and file paths (blank rejected client-side, otherwise sent verbatim — no silent trimming of legal POSIX filenames).
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### Transports
|
|
26
26
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- `tenki_git` accepts numbers and booleans as arg values (`depth: 1`, `create: true`) and coerces them to the strings the wire expects, instead of rejecting values that would serialize identically.
|
|
30
|
-
- **Shared `pathSchema`** (trimmed, non-empty) across every file-path argument (read/write/list/stat/mkdir/remove/move/upload/mount) — an empty or whitespace-only path is rejected client-side instead of by a server error.
|
|
31
|
-
- **`tenki_git` description documents a live-verified caveat:** checkout/diff/log run in the session's working directory and cannot target a repo cloned into a subdirectory (the API has no directory arg for them) — use `tenki_exec` with `git -C <dir> ...` for those until the API grows one. `clone` works as documented.
|
|
32
|
-
- **README**: install forms for Claude Code (`claude mcp add`), Claude Desktop, and Cursor; a consolidated environment-variable table; auth precedence documented (`TENKI_AUTH_TOKEN` wins over `TENKI_API_KEY`); the Tools table no longer lists unsupported git operations.
|
|
27
|
+
- **stdio** (default) and **Streamable HTTP** (`TENKI_MCP_TRANSPORT=http PORT=3000`), built from a shared `createServer()` factory.
|
|
28
|
+
- The HTTP transport is hardened: loopback-only by default (`TENKI_MCP_HTTP_HOST` to expose), bearer auth via `TENKI_MCP_HTTP_TOKEN` (constant-time checked; required to bind a non-loopback host), DNS-rebinding protection via a Host-header allowlist, a 1 MiB request-body cap enforced on both the header and the streamed byte count, session count cap with idle reaping, and SIGTERM/SIGINT graceful shutdown.
|
|
33
29
|
|
|
34
|
-
###
|
|
30
|
+
### Security posture
|
|
35
31
|
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
-
|
|
40
|
-
- New offline regression suite `test/client-net.test.mjs` (local `node:http` stub, zero external network) covering all of the above; wired into `npm test` and CI.
|
|
32
|
+
- **Tool annotations on every tool** (`readOnlyHint`, `destructiveHint`, `openWorldHint`) so clients can surface or gate dangerous tools.
|
|
33
|
+
- **Least-privilege env controls:** `TENKI_MCP_READONLY=1` registers only read tools (plus `tenki_auth_status`); `TENKI_MCP_DISABLED_TOOLS=a,b` drops named tools; `TENKI_MCP_AUDIT=1` logs each tool call's name and arg keys — never values, content, or tokens — to stderr. Applied centrally via a registration guard covering both SDK registration APIs.
|
|
34
|
+
- **SECURITY.md** documents the threat model, trust boundaries (the key and endpoint are capabilities; sandbox output is untrusted), and a CSA MCP Server Top-10 mapping.
|
|
35
|
+
- Importing the package cannot execute the server or exit the host process: the package is bin-only.
|
|
41
36
|
|
|
42
|
-
###
|
|
37
|
+
### Network layer
|
|
43
38
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- **
|
|
47
|
-
- **Behavior change:** `tenki_exec` / `tenki_run_code` results now report `ok: false` when the command ran but its output could not be read back (`captureError` set). Previously a capture failure with exit code 0 reported `ok: true` with empty stdout/stderr, which read as a clean silent success. `tenki_move_path` (exec-backed) is unaffected: its `ok` tracks the `mv` exit code, with any `captureError` surfaced separately.
|
|
48
|
-
- An unparseable exit code from the API is normalized to `-1` so the run's output is still returned instead of failing schema validation.
|
|
49
|
-
- Offline regression checks: `tenki_exec` advertises the schema, still carries guard annotations via the `registerTool` path, and exactly one tool declares an `outputSchema` (bump the count when migrating more tools). A new offline suite (`test/exec-output.test.mjs`) calls the tool in-memory and locks down the content blocks, escaping, output validation, and `tenki_move_path` semantics.
|
|
39
|
+
- **Every request carries a timeout** with one shared deadline per call (retries and backoff draw from it): 30s for unary calls (`TENKI_MCP_TIMEOUT_MS`), 600s for RPCs that block on storage work — snapshot creation and pause (`TENKI_MCP_SLOW_TIMEOUT_MS`) — and command execution follows the command's own timeout plus a 30s margin.
|
|
40
|
+
- **Retry policy shaped by double-apply risk:** methods that must never run twice (creates, builds, publishes, resumes, extends, exec) never retry transient failures; reads and idempotent teardown retry `unavailable`, gateway-shaped 502/503/504, and transport-level failures, with rate limits retried for every method under jittered backoff honoring `Retry-After`. Read-shaped data-plane methods retry under the same rules.
|
|
41
|
+
- **Session-credential cache** with expiry-aware refresh, single-flight minting, and invalidate-and-retry-once on a stale certificate (`permission_denied` deliberately does not re-mint).
|
|
50
42
|
|
|
51
|
-
###
|
|
43
|
+
### Client
|
|
52
44
|
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- **A path or working directory beginning with a hyphen is no longer read as a command option.** Shell-quoting doesn't prevent it: `cd '-L'` silently succeeds into `$HOME`, so `tenki_exec` with such a `cwd` ran the command in the wrong directory, and `mv '-file' …` failed outright. Both now pass `--` before their operands.
|
|
56
|
-
- **Sandbox creation was broken for workspace-scoped API keys.** `resolveOwner` forwarded WhoAmI's `ownerType` verbatim into `CreateSession`, but the API validates `owner_type ∈ {SERVICE, USER}` and now returns `WORKSPACE` for workspace-scoped keys → every `tenki_create_sandbox`/`tenki_run_code` failed with `400 invalid_argument`. Fix: send the same placeholder the first-party SDKs hardcode (`"SERVICE"`/`"self"`) when WhoAmI returns a type CreateSession rejects — the server derives the real owner from the authenticated identity regardless. Verified live: create → exec (structured) → terminate, 12/12 checks.
|
|
57
|
-
- `test/http-transport.test.mjs` no longer asserts `ownerType === "USER"` (stale against the same API change); it accepts any authenticated owner type.
|
|
58
|
-
|
|
59
|
-
## [2.0.0-alpha.2] — 2026-07-27 — MCP security hardening + registry fixes
|
|
60
|
-
|
|
61
|
-
### MCP security hardening (least privilege + annotations)
|
|
62
|
-
|
|
63
|
-
Applies MCP-native security controls, mapped to the CSA MCP Server Top-10 (esp. MCP-07 excessive permissions). See SECURITY.md.
|
|
64
|
-
|
|
65
|
-
- **Tool annotations** on all 84 tools (`readOnlyHint` on 36 read tools, `destructiveHint` on the 13 delete/terminate/revoke tools, `openWorldHint` on all) so clients can surface/gate dangerous tools.
|
|
66
|
-
- **Least-privilege env controls:** `TENKI_MCP_READONLY=1` registers only read tools (no create/run/delete/spend); `TENKI_MCP_DISABLED_TOOLS=a,b` drops named tools. Applied centrally via a registration guard — no change to the tool modules.
|
|
67
|
-
- **Audit logging:** `TENKI_MCP_AUDIT=1` logs each tool call name + arg keys (never values/content/token) to stderr.
|
|
68
|
-
- **SECURITY.md** — threat model, trust boundaries (the key + endpoint are capabilities; sandbox output is untrusted), and a full CSA Top-10 mapping. README security section + disclosure policy.
|
|
69
|
-
- Regression test `test/security.test.mjs` (10 offline checks).
|
|
70
|
-
|
|
71
|
-
### Registry/preview/artifact request-shape fixes
|
|
72
|
-
|
|
73
|
-
An API-contract review of the write paths found **11 more request-shape bugs** beyond the two already fixed — the registry module was almost entirely non-functional (it sent `reference` where the API wants `ref` / `imageId` / `imageRef`). All fixed and verified (each now reaches a clean not-found instead of a validation error); regression-guarded by test/registry-shapes.test.mjs.
|
|
74
|
-
|
|
75
|
-
- **registry** (9): `get_image`, `resolve_image_ref` (needs `ref`+`workspaceId`), `set_image_visibility` (needs `ref`+`REGISTRY_VISIBILITY_*` enum), `delete_image` (whole=`ref`; version=`imageId`+`snapshotId`), `share_image` (`imageRef`+`targetWorkspaceId`), `unshare_image` (`ref`), `revoke_image_share_grant` (`grantId`), `publish_image` (needs `ref`+`kind`+`snapshotId`/`sourceTemplateId`, not a session).
|
|
76
|
-
- **previews** (1): `touch_preview` takes a `previewToken`, not session/port.
|
|
77
|
-
- **artifacts** (1): `get_download_url` supports download-by-artifact-id only (the API rejects a path); the non-functional `path` option was removed.
|
|
78
|
-
|
|
79
|
-
## [2.0.0-alpha.1] — 2026-07-21 — Harden the HTTP transport (security)
|
|
80
|
-
|
|
81
|
-
An independent security review of the v2.0.0-alpha.0 HTTP transport found three coupled HIGH issues (the endpoint holds a shared TENKI_API_KEY and exposes code-execution + credit-spend tools). All fixed:
|
|
82
|
-
|
|
83
|
-
- **Loopback-only by default** — binds `127.0.0.1`, not `0.0.0.0`; the banner now shows the real host. Expose with `TENKI_MCP_HTTP_HOST`.
|
|
84
|
-
- **Bearer auth** — set `TENKI_MCP_HTTP_TOKEN` (constant-time checked); the server **refuses to bind to a non-loopback host without it**.
|
|
85
|
-
- **DNS-rebinding protection ON** — Host-header allowlist, so a malicious web page cannot drive the local server (verified: forged Host → 403).
|
|
86
|
-
|
|
87
|
-
Also: session count cap + idle reaping (init-flood DoS), a 1 MiB request-body cap enforced on both the Content-Length header and the streamed byte count (memory DoS), malformed JSON → JSON-RPC `-32700`/400, no internals in error responses, and SIGTERM/SIGINT graceful shutdown. Offline regression `test/http-input.test.mjs` asserts the 413 + a clean init; `test/http-transport.test.mjs` asserts the auth gate + rebinding rejection (7/7). stdio + tool parity unchanged (84).
|
|
88
|
-
|
|
89
|
-
## [2.0.0-alpha.0] — 2026-07-21 — HTTP transport (v2 begins)
|
|
90
|
-
|
|
91
|
-
The server now speaks **Streamable HTTP** in addition to stdio, so it can be hosted for remote MCP clients (`TENKI_MCP_TRANSPORT=http PORT=3000`). Verified end-to-end (connect → tools/list → tool call over HTTP; stdio unchanged, 84 tools). Server construction refactored into a shared `createServer()` factory (src/server.ts) used by both transports.
|
|
92
|
-
|
|
93
|
-
Streaming exec (`StreamCommandOutput`) and per-request HTTP auth are not yet implemented. Alpha: not for production hosting yet.
|
|
94
|
-
|
|
95
|
-
## [1.0.2] — 2026-07-21 — Fixes from comprehensive testing
|
|
96
|
-
|
|
97
|
-
A 34-scenario test matrix run against live Tenki found **two real request-shape bugs**, both fixed and verified end-to-end:
|
|
98
|
-
|
|
99
|
-
- **tenki_attach_volume** sent a flat request; AttachVolumeRequest nests the target under a `volume` sub-message (`{sessionId, volume:{volumeId, mountPath, readOnly?}}`). Every attach was rejected — this broke the volume warm-cache workflow. Fixed.
|
|
100
|
-
- **tenki_list_image_share_grants** sent `reference`; the API field is `ref` (required). It was silently ignored, 400-ing every call and making the ACL-read surface unreachable. Fixed.
|
|
101
|
-
|
|
102
|
-
Also: a real MCP **test suite** (`test/`) — a cleanup-safe harness driving the actual MCP protocol + 5 suites (coverage, client-integration, errors-edge, journeys, admin-previews) — **68 checks, all green**. Doc fixes (workspace tool names; create_template setup_script). No tool count change (84).
|
|
103
|
-
|
|
104
|
-
## [1.0.1] — 2026-07-20 — Fix ResizeVolume field
|
|
105
|
-
|
|
106
|
-
Live-verifying the volume write path (once a workspace volume-quota block was cleared) surfaced one real bug: `tenki_resize_volume` sent `sizeBytes` but the API expects `newSizeBytes`, so resizes were rejected. Fixed. Full volume lifecycle (create → get → update → resize → delete) now live-verified end-to-end.
|
|
107
|
-
|
|
108
|
-
## [1.0.0] — 2026-07-20 — Full CLI parity
|
|
109
|
-
|
|
110
|
-
**84 tools — parity with the entire Tenki unary API**, enforced by a CI parity audit (scripts/parity-audit.mjs fails the build if any SandboxService / DataPlane / SSHGateway method lacks a tool; streaming methods are deferred to v2.0). This release closes the long tail on top of v0.7: binary artifact transfer (get_upload_url / get_download_url), SSH access (update_ssh_keys / issue_ssh_cert / list_ssh_gateways), the preview-URL primitives (get/delete/touch/bind/unbind/resolve), project-scoped list variants (volumes/snapshots/templates), snapshot-retention settings, and registry grant-revoke. New read paths live-verified against api.tenki.cloud; write/advanced additions are grounded in the published API surface and labeled where not exercised end-to-end.
|
|
111
|
-
|
|
112
|
-
**Tools:** +18 to reach 84 (artifacts x2, ssh x3, preview extras x6, list variants x4, retention x2, revoke-grant x1)
|
|
113
|
-
|
|
114
|
-
## [0.7.0] — 2026-07-20 — Workspace administration
|
|
115
|
-
|
|
116
|
-
Workspace-level administration: sandbox usage reporting and get/update of workspace sandbox settings. Live-verified.
|
|
117
|
-
|
|
118
|
-
**Tools:** tenki_get_workspace_usage, tenki_get_workspace_settings, tenki_update_workspace_settings
|
|
119
|
-
|
|
120
|
-
## [0.6.0] — 2026-07-20 — Custom runtimes — templates & registry
|
|
121
|
-
|
|
122
|
-
Bring-your-own-runtime: define an environment once and boot into it warm. Templates add the platform's first async job surface (build, poll, cancel). The registry publishes versioned custom images with a private-by-default ACL surface. List paths live-verified.
|
|
123
|
-
|
|
124
|
-
**Tools:** 9 template tools (create/get/list/update/delete + build/cancel-build/get-build/list-active-builds) + 9 registry tools (publish/get/list/set-visibility/delete/delete-version/resolve-ref/share/list-share-grants)
|
|
125
|
-
|
|
126
|
-
## [0.5.0] — 2026-07-20 — Persistent state — snapshots & volumes
|
|
127
|
-
|
|
128
|
-
Persistent state for the iterative agent loop. Snapshots checkpoint a known-good sandbox to branch or roll back from; volumes are durable disks that carry a cache or dataset across otherwise-ephemeral sandboxes. Destructive verbs are explicit-target-only. Snapshots live-verified; volume shapes verified against the SDK (write path blocked only by a workspace volume quota during testing).
|
|
129
|
-
|
|
130
|
-
**Tools:** 8 snapshot tools + 8 volume tools (create/get/list/update/delete/resize/attach/detach)
|
|
131
|
-
|
|
132
|
-
## [0.4.0] — 2026-07-20 — Preview URLs — the ship surface
|
|
133
|
-
|
|
134
|
-
The ship surface: turn an exposed port into a public, shareable preview URL an agent can hand back. Project-scoped (live-verified: requires projectId + a validated slug). Completes the ports resource with unexpose.
|
|
135
|
-
|
|
136
|
-
**Tools:** tenki_create_preview_url, tenki_open_preview, tenki_list_preview_urls, tenki_unexpose_port
|
|
137
|
-
|
|
138
|
-
## [0.3.0] — 2026-07-20 — Session lifecycle & fleet control
|
|
139
|
-
|
|
140
|
-
Extended session control: extend a sandbox's wall-clock lifetime, update mutable fields (name/tags/idle-timeout/max-duration), bulk-terminate (explicit-id-only, irreversible), an activity heartbeat, and workspace/project-scoped fleet listing. Live-verified.
|
|
141
|
-
|
|
142
|
-
**Tools:** tenki_extend_sandbox, tenki_update_sandbox, tenki_terminate_sandboxes, tenki_report_sandbox_activity, tenki_list_workspace_sandboxes, tenki_list_project_sandboxes
|
|
143
|
-
|
|
144
|
-
## [0.2.0] — 2026-07-20 — Filesystem completion (data plane)
|
|
145
|
-
|
|
146
|
-
Data-plane filesystem metadata + mutation, completing the file surface beyond read/write/list: stat, mkdir (recursive), remove (recursive), and move (exec-backed mv, since the data plane exposes no Move RPC). Live-verified against api.tenki.cloud.
|
|
147
|
-
|
|
148
|
-
**Tools:** tenki_stat_path, tenki_make_dir, tenki_remove_path, tenki_move_path
|
|
149
|
-
|
|
150
|
-
## [0.1.0] — 2026-07-20
|
|
151
|
-
Initial release. **15 MCP tools over stdio, live-verified against `api.tenki.cloud`.**
|
|
152
|
-
|
|
153
|
-
- `tenki_whoami`
|
|
154
|
-
- `tenki_run_code` — ephemeral sandbox: boot → run shell/python/javascript → terminate
|
|
155
|
-
- Sandbox lifecycle: `tenki_create_sandbox`, `tenki_get_sandbox`, `tenki_list_sandboxes`, `tenki_terminate_sandbox`, `tenki_pause_sandbox`, `tenki_resume_sandbox`
|
|
156
|
-
- `tenki_exec` — run a command in a sandbox (stdout/stderr/exit inline)
|
|
157
|
-
- Files: `tenki_read_file`, `tenki_write_file`, `tenki_list_files`
|
|
158
|
-
- `tenki_git`
|
|
159
|
-
- Ports: `tenki_expose_port`, `tenki_list_exposed_ports`
|
|
160
|
-
|
|
161
|
-
Dependency-free ConnectRPC client (control + data plane), ported from the live-verified [n8n node](https://github.com/opencolin/n8n-nodes-tenki). Tools organized into self-registering modules under `src/tools/`.
|
|
45
|
+
- Dependency-free ConnectRPC client for the control and data planes; runtime dependencies are only `@modelcontextprotocol/sdk` and `zod`. Node ≥ 22.
|
|
46
|
+
- Offline regression suites (security, transport input handling, exec output, network behavior — local stubs, zero external network) run in CI via `npm test`; live end-to-end suites in `test/run.mjs`.
|
package/README.md
CHANGED
|
@@ -20,14 +20,32 @@ npx -y @tenkicloud/mcp # speaks MCP over stdio
|
|
|
20
20
|
|
|
21
21
|
Nothing to clone or build. The package installs one command, `tenki-mcp`.
|
|
22
22
|
|
|
23
|
-
> **Heads up — pending first npm publish.** Until `@tenkicloud/mcp` lands on npm, use [Run it from a clone](#run-it-from-a-clone-instead) below. Every `npx -y @tenkicloud/mcp` form here activates the moment it's published.
|
|
24
|
-
|
|
25
23
|
### Use it in Claude Code
|
|
26
24
|
|
|
25
|
+
Connect to Tenki's hosted MCP service and sign in with your Tenki account:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude mcp add --transport http tenki https://mcp.tenki.cloud/mcp
|
|
29
|
+
claude mcp login tenki
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Claude Code's `mcp add` command stores the server configuration; `mcp login` performs the one-time browser authorization and workspace selection.
|
|
33
|
+
|
|
34
|
+
Alternatively, run the MCP server locally with an API key:
|
|
35
|
+
|
|
27
36
|
```bash
|
|
28
37
|
claude mcp add tenki --env TENKI_API_KEY=tk_your_key_here -- npx -y @tenkicloud/mcp
|
|
29
38
|
```
|
|
30
39
|
|
|
40
|
+
### Or install it as a Claude Code plugin
|
|
41
|
+
|
|
42
|
+
The repo doubles as a plugin marketplace. This prompts for your API key on install and stores it in your OS keychain — no env var to manage:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/plugin marketplace add LuxorLabs/tenki-mcp
|
|
46
|
+
/plugin install tenki@tenki
|
|
47
|
+
```
|
|
48
|
+
|
|
31
49
|
### Use it in Claude Desktop
|
|
32
50
|
|
|
33
51
|
Add to `claude_desktop_config.json`:
|
|
@@ -108,10 +126,16 @@ Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` comman
|
|
|
108
126
|
| `PORT` | `3000` | HTTP transport port. |
|
|
109
127
|
| `TENKI_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind host; non-loopback requires `TENKI_MCP_HTTP_TOKEN`. |
|
|
110
128
|
| `TENKI_MCP_HTTP_TOKEN` | — | Bearer token for the HTTP endpoint; optional on loopback, required on a non-loopback host. |
|
|
129
|
+
| `TENKI_MCP_PUBLIC_URL` | — | Public base URL for an OAuth-protected hosted server. |
|
|
130
|
+
| `TENKI_MCP_OAUTH_ISSUER` | — | OAuth authorization-server issuer. Enables delegated OAuth HTTP mode. |
|
|
131
|
+
| `TENKI_MCP_OAUTH_RESOURCE` | `<public URL>/mcp` | RFC 8707 resource identifier accepted in access-token audiences. |
|
|
132
|
+
| `TENKI_MCP_OAUTH_SCOPE` | `mcp` | Required delegated scope. |
|
|
133
|
+
| `TENKI_MCP_IDENTITY_URL` | — | Internal Tenki Identity service endpoint used to exchange OAuth access tokens. |
|
|
134
|
+
| `TENKI_MCP_IDENTITY_SERVICE_TOKEN` | — | Service credential for the private Identity token-exchange RPC. |
|
|
111
135
|
|
|
112
136
|
## Tools
|
|
113
137
|
|
|
114
|
-
**
|
|
138
|
+
**71 tools** — all 68 public unary API methods (enforced by a CI [parity audit](scripts/parity-audit.mjs)), two workflow helpers, and `tenki_auth_status`. Implementation-only control-plane methods are intentionally excluded. Grouped by domain:
|
|
115
139
|
|
|
116
140
|
| Domain | Tools |
|
|
117
141
|
|---|---|
|
|
@@ -119,17 +143,16 @@ Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` comman
|
|
|
119
143
|
| **Identity** | `tenki_whoami` |
|
|
120
144
|
| **Run** | `tenki_run_code` (one-shot: boot → run shell/python/js → tear down) |
|
|
121
145
|
| **Sandboxes** | `tenki_create_sandbox` · `tenki_get_sandbox` · `tenki_list_sandboxes` · `tenki_terminate_sandbox` · `tenki_pause_sandbox` · `tenki_resume_sandbox` |
|
|
122
|
-
| **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes`
|
|
146
|
+
| **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes` |
|
|
123
147
|
| **Exec** | `tenki_exec` (stdout/stderr/exit inline) |
|
|
124
148
|
| **Files** | `tenki_read_file` · `tenki_write_file` · `tenki_list_files` · `tenki_stat_path` · `tenki_make_dir` · `tenki_remove_path` · `tenki_move_path` |
|
|
125
149
|
| **Git** | `tenki_git` (clone/checkout/diff/log — the API supports exactly these four; run other git commands via `tenki_exec`) |
|
|
126
150
|
| **Ports & previews** | expose · list-exposed · unexpose · create-preview-url · open-preview · list/get/delete-preview-url · touch-preview · bind/unbind-preview-url · resolve-preview-token |
|
|
127
151
|
| **Artifacts** (binary transfer) | `tenki_get_upload_url` · `tenki_get_download_url` (signed URLs for binary PUT/GET) |
|
|
128
152
|
| **SSH** | `tenki_update_ssh_keys` · `tenki_issue_ssh_cert` · `tenki_list_ssh_gateways` |
|
|
129
|
-
| **Snapshots** | create · get · list · list-session · list-
|
|
130
|
-
| **Volumes** | create · get · list ·
|
|
131
|
-
| **Templates** | create · get · list ·
|
|
132
|
-
| **Registry** (custom images) | publish · get · list · set-visibility · delete · resolve-ref · share · unshare · list-share-grants · revoke-share-grant |
|
|
153
|
+
| **Snapshots** | create · get · list · list-session · list-workspace · list-dangling · update · delete · get-download-url |
|
|
154
|
+
| **Volumes** | create · get · list · update · delete · resize · attach · detach |
|
|
155
|
+
| **Templates** | create · get · list · update · delete · build · cancel-build · get-build · list-active-builds |
|
|
133
156
|
| **Workspace** | `tenki_get_workspace_usage` · `tenki_get_workspace_settings` · `tenki_update_workspace_settings` · `tenki_get_snapshot_retention_settings` · `tenki_update_snapshot_retention_settings` |
|
|
134
157
|
|
|
135
158
|
Full per-release breakdown in [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -149,7 +172,7 @@ TENKI_MCP_TRANSPORT=http PORT=3000 TENKI_API_KEY=… npx -y @tenkicloud/mcp
|
|
|
149
172
|
# → tenki-mcp running on http://127.0.0.1:3000/mcp (Streamable HTTP) [loopback only, no auth]
|
|
150
173
|
```
|
|
151
174
|
|
|
152
|
-
|
|
175
|
+
For a single-user deployment, HTTP mode can hold one shared `TENKI_API_KEY` and therefore exposes a powerful capability. By default it:
|
|
153
176
|
|
|
154
177
|
- **binds to loopback (`127.0.0.1`) only** — set `TENKI_MCP_HTTP_HOST=0.0.0.0` to expose it, but then
|
|
155
178
|
- it **requires a bearer token**: set `TENKI_MCP_HTTP_TOKEN` and send `Authorization: Bearer <token>`. It **refuses to start** on a non-loopback host without one.
|
|
@@ -161,7 +184,9 @@ TENKI_MCP_TRANSPORT=http TENKI_MCP_HTTP_HOST=0.0.0.0 PORT=3000 \
|
|
|
161
184
|
TENKI_MCP_HTTP_TOKEN=$(openssl rand -hex 32) TENKI_API_KEY=… npx -y @tenkicloud/mcp
|
|
162
185
|
```
|
|
163
186
|
|
|
164
|
-
Point an HTTP-capable MCP client at `/mcp`.
|
|
187
|
+
Point an HTTP-capable MCP client at `/mcp`. Static-key mode uses one shared `TENKI_API_KEY` for all sessions. Verified end-to-end (`test/http-transport.test.mjs`: auth gate, DNS-rebinding rejection, connect → tools/list → tool call over HTTP).
|
|
188
|
+
|
|
189
|
+
Hosted multi-tenant deployments instead use Tenki Identity's OAuth facade. The MCP server publishes RFC 9728 protected-resource metadata and exchanges each caller's access token through the private Identity service. Identity owns Hydra, validates the requested audience and scope, and issues a short-lived API delegation bound to the user, client, and workspace selected on the Tenki consent page. `tenki-mcp` has no Hydra Admin access and does not hold the delegation-signing secret.
|
|
165
190
|
|
|
166
191
|
## How it works
|
|
167
192
|
|
package/SECURITY.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Security
|
|
2
2
|
|
|
3
|
-
`tenki-mcp` gives an AI agent a capability: a disposable microVM it can create, run code in, and spend Tenki credits with. Treat the server
|
|
3
|
+
`tenki-mcp` gives an AI agent a capability: a disposable microVM it can create, run code in, and spend Tenki credits with. Treat the server and its credentials accordingly. This document is the threat model and the controls, mapped to the [CSA MCP Server Top 10](https://modelcontextprotocol-security.io/top10/server/).
|
|
4
4
|
|
|
5
5
|
## Reporting a vulnerability
|
|
6
6
|
Please **do not** open a public issue for security reports. Open a private [GitHub security advisory](https://github.com/LuxorLabs/tenki-mcp/security/advisories/new), or email **hello@luxor.tech**. We'll acknowledge within a few business days.
|
|
7
7
|
|
|
8
8
|
## Trust boundaries (read this first)
|
|
9
|
-
- **
|
|
9
|
+
- **Credentials are capabilities.** Local and single-user modes authenticate with a `TENKI_API_KEY` or session token. Hosted OAuth mode exchanges each caller's token through Tenki Identity and receives only a short-lived, workspace-bound internal delegation to the Tenki API. Never commit credentials (`.env` is gitignored).
|
|
10
10
|
- **Sandbox output is untrusted.** `tenki_run_code` / `tenki_exec` / `tenki_read_file` return output produced by *untrusted, AI-generated code running in the sandbox*. That output flows back to the calling model as a tool result — a classic **indirect / output prompt-injection** vector. The microVM is the isolation boundary; the model should treat tool results as **data, not instructions**. (MCP clients are responsible for not executing instructions found in tool output.)
|
|
11
|
-
- **The HTTP endpoint is a capability.** In HTTP mode
|
|
11
|
+
- **The HTTP endpoint is a capability.** In static-key HTTP mode, access to `/mcp` is equivalent to access to the configured key. Hosted mode requires a valid OAuth bearer on every request and binds the MCP session to its user, client, and workspace.
|
|
12
12
|
|
|
13
13
|
## Controls this server provides
|
|
14
14
|
|
|
@@ -28,7 +28,7 @@ Grant the smallest set that the use case needs.
|
|
|
28
28
|
- **HTTP** (`TENKI_MCP_TRANSPORT=http`) binds **loopback-only** by default, **requires a bearer token** to bind to a non-loopback host, has **DNS-rebinding protection**, and caps sessions + body size. For network exposure put it behind a **TLS-terminating proxy**. (Hardening details in the transport module, `src/http.ts`.)
|
|
29
29
|
|
|
30
30
|
### Secrets & audit (MCP-04, Observability)
|
|
31
|
-
-
|
|
31
|
+
- Static credentials are read from env and sent only as auth headers. Hosted OAuth tokens are sent only to the private Tenki Identity exchange endpoint and are never forwarded to the Tenki API; the API receives a short-lived signed delegation instead.
|
|
32
32
|
- `TENKI_MCP_AUDIT=1` logs each tool call's **name + argument keys** to stderr (never values, content, or the token) for an operator audit trail.
|
|
33
33
|
|
|
34
34
|
## CSA MCP Server Top-10 mapping
|
|
@@ -36,9 +36,9 @@ Grant the smallest set that the use case needs.
|
|
|
36
36
|
| # | Risk | tenki-mcp posture |
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| MCP-01 | Prompt Injection | zod-validates every tool arg pre-network; **sandbox output is untrusted** (treat tool results as data) |
|
|
39
|
-
| MCP-02 | Confused Deputy |
|
|
39
|
+
| MCP-02 | Confused Deputy | static mode requires its own HTTP bearer; hosted mode authenticates every request and binds the session and API delegation to the authorized user, client, and workspace; `READONLY`/denylist further bound the blast radius |
|
|
40
40
|
| MCP-03 | Tool Poisoning | tool descriptions are static and authored (no dynamic/remote descriptions); verify the package via its npm provenance attestation (published from GitHub Actions, linking each release to its source commit) + MCP-registry namespace ownership |
|
|
41
|
-
| MCP-04 | Credential/Token Exposure |
|
|
41
|
+
| MCP-04 | Credential/Token Exposure | credentials are never logged/committed/echoed; hosted OAuth tokens go only to Tenki Identity and only short-lived internal delegations reach the API; audit logs keys not values |
|
|
42
42
|
| MCP-05 | Insecure Configuration | HTTP transport is loopback + token + DNS-rebinding-protected + DoS-capped by default |
|
|
43
43
|
| MCP-06 | Supply Chain | 2 direct runtime deps (`@modelcontextprotocol/sdk`, `zod`) — ~91 transitive, nearly all via the MCP SDK; lockfile committed; released only from CI with npm provenance, Actions pinned to commit SHAs |
|
|
44
44
|
| MCP-07 | Excessive Permissions | tool annotations + `TENKI_MCP_READONLY` + `TENKI_MCP_DISABLED_TOOLS` |
|
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface ExecResult {
|
|
|
21
21
|
exitCode: number;
|
|
22
22
|
ok: boolean;
|
|
23
23
|
captureError?: string;
|
|
24
|
+
stdoutTruncated?: boolean;
|
|
25
|
+
stderrTruncated?: boolean;
|
|
26
|
+
/** Sandbox path holding the FULL stdout, present only when stdout was truncated. */
|
|
27
|
+
stdoutPath?: string;
|
|
28
|
+
/** Sandbox path holding the FULL stderr, present only when stderr was truncated. */
|
|
29
|
+
stderrPath?: string;
|
|
24
30
|
}
|
|
25
31
|
export interface TenkiClientOptions {
|
|
26
32
|
/** Timeout for unary control/data-plane calls (default 30s). */
|
|
@@ -31,6 +37,10 @@ export interface TenkiClientOptions {
|
|
|
31
37
|
slowTimeoutMs?: number;
|
|
32
38
|
/** Assumed session-credential lifetime when the API returns no parseable expiry (default 5min). */
|
|
33
39
|
credTtlMs?: number;
|
|
40
|
+
/** Workspace selected by an upstream delegated authorization grant. */
|
|
41
|
+
workspaceId?: string;
|
|
42
|
+
/** Supplies a short-lived Bearer credential for hosted delegated calls. */
|
|
43
|
+
bearerTokenProvider?: () => string;
|
|
34
44
|
}
|
|
35
45
|
export declare class TenkiClient {
|
|
36
46
|
private readonly token;
|
|
@@ -41,7 +51,10 @@ export declare class TenkiClient {
|
|
|
41
51
|
private readonly execTimeoutMs;
|
|
42
52
|
private readonly slowTimeoutMs;
|
|
43
53
|
private readonly credTtlMs;
|
|
54
|
+
private readonly workspaceId?;
|
|
55
|
+
private readonly bearerTokenProvider?;
|
|
44
56
|
constructor(token: string, baseUrl?: string, opts?: TenkiClientOptions);
|
|
57
|
+
private controlAuthHeaders;
|
|
45
58
|
/**
|
|
46
59
|
* ExecuteCommand blocks until the command finishes, so its timeout follows
|
|
47
60
|
* the command's own timeout (plus margin) instead of the unary default.
|
|
@@ -98,9 +111,10 @@ export declare class TenkiClient {
|
|
|
98
111
|
*/
|
|
99
112
|
data(sessionId: string, method: string, request?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
100
113
|
/**
|
|
101
|
-
* Resolve the calling identity + a default workspace
|
|
102
|
-
*
|
|
103
|
-
*
|
|
114
|
+
* Resolve the calling identity + a default workspace for CreateSession and
|
|
115
|
+
* other workspace-scoped calls. (Projects were removed from the API — the
|
|
116
|
+
* proto reserves every project_id field and WhoAmI no longer lists projects —
|
|
117
|
+
* so there is nothing narrower than a workspace to resolve.)
|
|
104
118
|
*
|
|
105
119
|
* CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
|
|
106
120
|
* non-empty owner_id, but derives the real owner from the authenticated
|
|
@@ -112,7 +126,6 @@ export declare class TenkiClient {
|
|
|
112
126
|
ownerType?: string;
|
|
113
127
|
ownerId?: string;
|
|
114
128
|
workspaceId?: string;
|
|
115
|
-
projectId?: string;
|
|
116
129
|
}>;
|
|
117
130
|
/** Poll GetSession until it reaches (or passes into) the target state. */
|
|
118
131
|
waitForState(sessionId: string, target?: string, { timeoutMs, intervalMs }?: {
|
|
@@ -121,6 +134,15 @@ export declare class TenkiClient {
|
|
|
121
134
|
}): Promise<Record<string, any>>;
|
|
122
135
|
readTextFile(sessionId: string, path: string): Promise<string>;
|
|
123
136
|
writeTextFile(sessionId: string, path: string, text: string): Promise<Record<string, any>>;
|
|
137
|
+
/**
|
|
138
|
+
* Read a capture file back, capping what crosses the wire. Stat first: a
|
|
139
|
+
* file at or under the cap is read whole; a larger one gets a server-side
|
|
140
|
+
* head+tail preview (assembled by `sh` in the sandbox, so the full body
|
|
141
|
+
* never leaves it — ReadFile has no range support). The preview file is the
|
|
142
|
+
* caller's to clean up (returned as previewPath); the ORIGINAL is theirs to
|
|
143
|
+
* keep, so the user can page through the full output afterwards.
|
|
144
|
+
*/
|
|
145
|
+
private readCaptureFile;
|
|
124
146
|
/**
|
|
125
147
|
* Run a command in a session and return stdout/stderr inline.
|
|
126
148
|
*
|
|
@@ -129,13 +151,16 @@ export declare class TenkiClient {
|
|
|
129
151
|
* client. Capture-read failures degrade gracefully into `captureError` rather
|
|
130
152
|
* than losing the run — but the result is marked NOT ok, because empty
|
|
131
153
|
* stdout/stderr next to a zero exit code would otherwise read as a clean,
|
|
132
|
-
* silent success.
|
|
154
|
+
* silent success. Streams over maxOutputBytes come back truncated
|
|
155
|
+
* (head+tail) with the full capture file retained in the sandbox — see
|
|
156
|
+
* readCaptureFile; truncation does not affect `ok`.
|
|
133
157
|
*/
|
|
134
158
|
execCaptured(sessionId: string, command: string, opts?: {
|
|
135
159
|
args?: string[];
|
|
136
160
|
cwd?: string;
|
|
137
161
|
env?: Record<string, string>;
|
|
138
162
|
timeoutSeconds?: number;
|
|
163
|
+
maxOutputBytes?: number;
|
|
139
164
|
}): Promise<ExecResult>;
|
|
140
165
|
/**
|
|
141
166
|
* One-shot: boot a throwaway sandbox, run code (shell / python / javascript),
|