@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 CHANGED
@@ -1,161 +1,46 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to tenki-mcp. This project follows semantic versioning.
3
+ All notable changes to `@tenkicloud/mcp`. This project follows semantic versioning.
4
4
 
5
- ## [0.1.0] — 2026-08-03 — First npm release: typed git, network hardening, structured output
5
+ ## [0.2.0] — 2026-08-21
6
6
 
7
- Published as **`@tenkicloud/mcp`**, matching the `@tenkicloud/sandbox` SDK. Install with `npx -y @tenkicloud/mcp`; the command it provides is still `tenki-mcp`.
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
- ### Fixed by an end-to-end sweep of all 85 tools against the live API
13
+ ## [0.1.0] 2026-08-03 Initial release
10
14
 
11
- - **`tenki_create_snapshot` and `tenki_pause_sandbox` reported failure on operations that succeeded.** Both RPCs block until the underlying storage work finishes measured at 59s and 41s so the 30s default HTTP budget introduced with the timeout work cut them off. The caller got `timed out after 30000ms` while the snapshot (or a multi-GB pause snapshot) had in fact been created, leaving an orphaned resource whose id it never saw, and an agent that retried burned quota. Both now use a 600s budget, tunable with `TENKI_MCP_SLOW_TIMEOUT_MS` (`TENKI_MCP_TIMEOUT_MS` tunes the ordinary 30s one). Async methods that return a handle in under a second — `BuildTemplate`, `PublishRegistryImage` — deliberately keep the short budget.
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 Clouddisposable 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
- ### A missing credential no longer looks like a broken server
17
+ ### Tools
20
18
 
21
- - **New tool `tenki_auth_status`** (85 tools total) reports whether a usable credential is configured, which kind (`api_key` for `tk_…`, `oauth_session_token` for `ory_st_…`, or `session_cookie`), which env var it came from, the endpoint targeted, and whether a live identity probe succeeded. Never returns the token. Structured output, annotated read-only, and available under `TENKI_MCP_READONLY` (it is how you diagnose a credential problem in that posture) — `TENKI_MCP_DISABLED_TOOLS` can still drop it.
22
- - **The server starts without a credential instead of exiting 1.** Exiting made MCP clients report an opaque "server failed to start" while swallowing the stderr line that explained why, leaving the user with a broken server and no cause. It now boots with `tenki_auth_status` as the only registered tool, so an agent can ask what is wrong and relay the fix; the reported `toolsRegistered` count makes that degraded mode explicit. Registering all 84 API tools in that state would only offer tools that cannot work.
23
- - Reporting status is the whole scope: nothing here logs in, opens a browser, or writes credentials. Get a credential with `tenki login` or from the dashboard.
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
- ### Tool-schema DX (git enum, shared schemas, README)
25
+ ### Transports
26
26
 
27
- - **`tenki_git` now validates `operation` as an enum of what the API actually supports: `clone`, `checkout`, `diff`, `log`.** The tool previously advertised ten operations, but six of them (status/add/commit/pull/push/fetchPR) are rejected by the API's own validation (`GitOperationRequest` allows exactly four) a model following the old description was guaranteed a server error. Per-operation arg keys are now documented in the tool description, grounded in the engine's arg builder: clone `{repo, branch?, depth?, directory?}`, checkout `{ref, create?}`, diff `{range? | base?+head?, path?}`, log `{max_count?, range?, path?}`. Other git commands: use `tenki_exec` with `git ...`.
28
- - **Shared input schemas** in `common.ts`: `sessionIdSchema` (described; trimmed and non-empty, so a whitespace-only id is rejected client-side — previously bare and undescribed in 8 of 14 modules) — reused across ports/previews/files/exec/ssh/snapshots/artifacts/sandboxes/volumes/sessions-admin, including the `session_ids` array in `tenki_terminate_sandboxes`; `portSchema` (1-65535 `tenki_expose_port` previously accepted port 99999 and learned the constraint from a server error) ports; `slugSchema` (3-63 chars, lowercase/digits/hyphens, no leading/trailing hyphen matching the server's shared preview-slug validation, which `ExposePort` also applies) ports/previews.
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
- ### Network-layer hardening (timeouts, retry policy, credential cache)
30
+ ### Security posture
35
31
 
36
- - **Every fetch now carries a timeout** — a hung control- or data-plane connection, or a response whose body stalls after the headers arrive, fails the tool call with a clear `timed out after Nms` error naming the method instead of blocking it forever. Unary calls default to 30s; `ExecuteCommand` follows the command's own timeout + 30s margin (630s when the command has none). Tunable via `TenkiClient` options. Each call has ONE shared deadline: retries and backoff sleeps draw from it, so a call can never stack attempts past its timeout.
37
- - **Retry policy is shaped by double-apply risk, not by method naming.** Methods that must never run twice (`Create*`, `Build*`, `Publish*`, `Resume*`, `Extend*`, `ExecuteCommand`) never retry transient failures — a half-applied `CreateSession` retried could boot and bill a second sandbox. Everything else reads AND teardown (`Terminate*`/`Delete*`/`Detach*`, where a repeat lands in the same state) retries `unavailable`, gateway-shaped 502/503/504 (HTML/empty bodies from load balancers included), and transport-level failures (timeout, connection reset, DNS). Rate-limit rejections still retry for every method, with jittered backoff and `Retry-After` honored (capped at 30s).
38
- - **The data plane retries too.** Read-shaped data-plane methods (`ReadFile`/`Stat`/`List*`) — the path behind every file tool and how `tenki_exec` reads back its own output now retry rate limits, transient/gateway errors, and transport failures under the same shared deadline. Write-shaped data methods surface failures immediately.
39
- - **Session-credential cache can no longer go permanently stale.** A credential whose expiry the API omits (or that fails to parse) was previously cached *forever* — once the cert actually expired, every file operation for that session failed for the life of the process. Now: missing expiry → 5-minute TTL; known expiry → refreshed 30s early (floored at 5s so a short-lived credential still caches; a credential already past its expiry is not cached at all); a stale-certificate failure (401/`unauthenticated`) invalidates the cached credential and retries once with a fresh one (never loops; `permission_denied` does NOT re-mint — a denied path is a user error a fresh cert can't fix); and minting is single-flight, so concurrent cold-cache calls share one mint.
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 keysnever values, content, or tokensto 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
- ### Structured tool output (first tool: `tenki_exec`)
37
+ ### Network layer
43
38
 
44
- - **`tenki_exec` now declares an `outputSchema` and returns `structuredContent`**, so MCP clients can consume `stdout`/`stderr`/`exitCode`/`ok` as typed JSON instead of re-parsing a stringified blob. The SDK validates every successful result against the schema.
45
- - **`tenki_exec` returns two text blocks:** the serialized JSON result first (the MCP spec's backwards-compatibility contract for structured content existing text-parsing consumers keep working), then a human-readable rendering headed by `exit <code>` and character-count markers. Characters that can misrepresent output in a terminal — C0/C1 controls, bidi overrides, zero-width marks are escaped to visible `\xNN`/`\uNNNN` in the rendering (sandbox output is untrusted); `structuredContent` keeps the raw strings.
46
- - **Registration guard extended to `registerTool`** — the modern SDK registration API (required for `outputSchema`) now passes through the same least-privilege guard as the legacy `.tool()` form: name-derived annotations, `TENKI_MCP_READONLY`, `TENKI_MCP_DISABLED_TOOLS`, and `TENKI_MCP_AUDIT` all apply. Previously a module using `registerTool` would have silently bypassed all four.
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
- ### Fixed
43
+ ### Client
52
44
 
53
- - **Path validation no longer rewrites the path.** `pathSchema` used zod's `.trim()`, which is a *transform*: leading/trailing whitespace (legal in POSIX filenames) was silently stripped before sending, so `tenki_remove_path`/`tenki_move_path` on such a file operated on a *different* path than the one named. Paths now reach the API verbatim, with blank/whitespace-only values still rejected client-side. `tenki_exec`'s `cwd` had the same trim and is fixed too. (`sessionIdSchema` keeps trimming: a session id is a UUID, so surrounding whitespace is always an accident.)
54
- - **Importing the package no longer terminates the importing process.** `main` pointed at the CLI entrypoint, whose token check runs at module load and calls `process.exit(1)` so `import "tenki-mcp"` killed the host process outright (and with a token set, would have booted a stdio server inside it). The package is now bin-only: `npx tenki-mcp` and every MCP client config are unaffected (they resolve `bin`), and importing raises a catchable module-not-found error instead.
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 behaviorlocal 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
- **85 tools** — 84 with full parity to the Tenki unary API (enforced by a CI [parity audit](scripts/parity-audit.mjs)), plus `tenki_auth_status`. Grouped by domain:
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` · `tenki_list_project_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-project · list-workspace · list-dangling · update · delete · get-download-url |
130
- | **Volumes** | create · get · list · list-project · update · delete · resize · attach · detach |
131
- | **Templates** | create · get · list · list-project · update · delete · build · cancel-build · get-build · list-active-builds |
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
- **Security — this endpoint is a capability.** In HTTP mode the process holds one shared `TENKI_API_KEY` and exposes every tool, including arbitrary code execution and credit spend. So by default it:
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`. v2.0-beta uses one shared `TENKI_API_KEY` for all sessions; per-request auth (multi-tenant hosting) is not yet implemented. Verified end-to-end (`test/http-transport.test.mjs`: auth gate, DNS-rebinding rejection, connect → tools/list → tool call over HTTP).
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 and the API key it holds — accordingly. This document is the threat model and the controls, mapped to the [CSA MCP Server Top 10](https://modelcontextprotocol-security.io/top10/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
- - **The API key is a capability.** In every mode the process authenticates to Tenki with one `TENKI_API_KEY` and can create sandboxes, run arbitrary code, and spend credits. Anyone who can invoke the server can do those things. Scope the key if your Tenki plan allows it; never commit it (`.env` is gitignored).
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 the `/mcp` endpoint is equivalent to handing out the key protect it (below).
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
- - The key is read from env and sent only as an auth header never logged, never in error responses.
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 | single shared key; HTTP endpoint requires its own bearer token; `READONLY`/denylist bound the blast radius; per-request auth is not yet implemented |
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 | key via env only; never logged/committed/echoed; audit logs keys not values |
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/project for CreateSession
102
- * (which requires a projectId). Picks the first workspace that has a project so
103
- * the (workspace, project) pair stays consistent.
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),