@luxorai/mcp 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +216 -2
  4. package/SECURITY.md +50 -0
  5. package/dist/client.d.ts +177 -0
  6. package/dist/client.js +575 -0
  7. package/dist/http.d.ts +17 -0
  8. package/dist/http.js +294 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +60 -0
  11. package/dist/oauth.d.ts +33 -0
  12. package/dist/oauth.js +146 -0
  13. package/dist/server.d.ts +30 -0
  14. package/dist/server.js +203 -0
  15. package/dist/tools/artifacts.d.ts +16 -0
  16. package/dist/tools/artifacts.js +19 -0
  17. package/dist/tools/auth_status.d.ts +35 -0
  18. package/dist/tools/auth_status.js +109 -0
  19. package/dist/tools/common.d.ts +35 -0
  20. package/dist/tools/common.js +60 -0
  21. package/dist/tools/exec.d.ts +4 -0
  22. package/dist/tools/exec.js +120 -0
  23. package/dist/tools/files.d.ts +4 -0
  24. package/dist/tools/files.js +22 -0
  25. package/dist/tools/files_ops.d.ts +12 -0
  26. package/dist/tools/files_ops.js +54 -0
  27. package/dist/tools/git.d.ts +4 -0
  28. package/dist/tools/git.js +30 -0
  29. package/dist/tools/identity.d.ts +4 -0
  30. package/dist/tools/identity.js +5 -0
  31. package/dist/tools/ports.d.ts +4 -0
  32. package/dist/tools/ports.js +6 -0
  33. package/dist/tools/previews.d.ts +18 -0
  34. package/dist/tools/previews.js +80 -0
  35. package/dist/tools/run.d.ts +4 -0
  36. package/dist/tools/run.js +11 -0
  37. package/dist/tools/sandboxes.d.ts +4 -0
  38. package/dist/tools/sandboxes.js +72 -0
  39. package/dist/tools/sessions_admin.d.ts +9 -0
  40. package/dist/tools/sessions_admin.js +62 -0
  41. package/dist/tools/snapshots.d.ts +11 -0
  42. package/dist/tools/snapshots.js +79 -0
  43. package/dist/tools/ssh.d.ts +15 -0
  44. package/dist/tools/ssh.js +17 -0
  45. package/dist/tools/templates.d.ts +14 -0
  46. package/dist/tools/templates.js +142 -0
  47. package/dist/tools/volumes.d.ts +16 -0
  48. package/dist/tools/volumes.js +77 -0
  49. package/dist/tools/workspace.d.ts +4 -0
  50. package/dist/tools/workspace.js +129 -0
  51. package/package.json +59 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@tenkicloud/mcp`. This project follows semantic versioning.
4
+
5
+ ## [0.2.0] — 2026-08-21
6
+
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`.
12
+
13
+ ## [0.1.0] — 2026-08-03 — Initial release
14
+
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`.
16
+
17
+ ### Tools
18
+
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
+
25
+ ### Transports
26
+
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.
29
+
30
+ ### Security posture
31
+
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.
36
+
37
+ ### Network layer
38
+
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).
42
+
43
+ ### Client
44
+
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tenki Cloud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,217 @@
1
- # Temporary Holding Version
1
+ # tenki-mcp — `@tenkicloud/mcp`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A [Model Context Protocol](https://modelcontextprotocol.io) server for [Tenki Cloud](https://tenki.cloud).** Give any agent — Claude, Codex, Cursor — a disposable microVM it can create, run code in, read and write files, run git, and expose to the web. Sandboxes boot in ~2 seconds and are billed per second.
4
+
5
+ Part of making Tenki the execution layer coding agents reach for: the agent writes code and Tenki runs it in isolation. (Tenki's **Code Reviewer** and **Runners** — AI PR review and managed CI — are separate products; this server currently exposes the Sandbox.)
6
+
7
+ ```
8
+ "Run this Python in a fresh sandbox and tell me what it prints."
9
+ │
10
+ ▼ tenki_run_code
11
+ boots a microVM → runs it → returns stdout → tears it down
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```bash
17
+ export TENKI_API_KEY=tk_your_key_here
18
+ npx -y @tenkicloud/mcp # speaks MCP over stdio
19
+ ```
20
+
21
+ Nothing to clone or build. The package installs one command, `tenki-mcp`.
22
+
23
+ ### Use it in Claude Code
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
+
36
+ ```bash
37
+ claude mcp add tenki --env TENKI_API_KEY=tk_your_key_here -- npx -y @tenkicloud/mcp
38
+ ```
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
+
49
+ ### Use it in Claude Desktop
50
+
51
+ Add to `claude_desktop_config.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "tenki": {
57
+ "command": "npx",
58
+ "args": ["-y", "@tenkicloud/mcp"],
59
+ "env": { "TENKI_API_KEY": "tk_your_key_here" }
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### Use it in Cursor
66
+
67
+ Add the same block to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "tenki": {
73
+ "command": "npx",
74
+ "args": ["-y", "@tenkicloud/mcp"],
75
+ "env": { "TENKI_API_KEY": "tk_your_key_here" }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Use it in Codex
82
+
83
+ The OpenAI Codex CLI reads MCP servers from `~/.codex/config.toml`. Add:
84
+
85
+ ```toml
86
+ [mcp_servers.tenki]
87
+ command = "npx"
88
+ args = ["-y", "@tenkicloud/mcp"]
89
+ env = { TENKI_API_KEY = "tk_your_key_here" }
90
+ ```
91
+
92
+ ### Confirm it's working
93
+
94
+ After adding the server, **start a fresh session** so the client loads it, then ask your agent one of:
95
+
96
+ - *"Check tenki auth status."* — confirms your credential was picked up.
97
+ - *"Run `print(2+2)` in a fresh Tenki sandbox."* — a full boot → run → teardown round-trip.
98
+
99
+ If the `tenki_*` tools don't show up, restart the client so it re-reads its config.
100
+
101
+ No key yet? Start the server without one and ask the agent to check `tenki_auth_status` — it reports what to set and where. See [Auth](#auth).
102
+
103
+ ### Run it from a clone instead
104
+
105
+ For development, or to run an unreleased change:
106
+
107
+ ```bash
108
+ git clone https://github.com/LuxorLabs/tenki-mcp.git && cd tenki-mcp
109
+ npm install && npm run build
110
+ TENKI_API_KEY=tk_your_key_here node dist/index.js
111
+ ```
112
+
113
+ Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` command in any of the configs above. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development loop.
114
+
115
+ ### Environment variables
116
+
117
+ | Variable | Default | Purpose |
118
+ |---|---|---|
119
+ | `TENKI_API_KEY` | — | API key (`tk_…`). One of this or `TENKI_AUTH_TOKEN` is required. |
120
+ | `TENKI_AUTH_TOKEN` | — | Session token (`ory_st_…` or cookie value). **Takes precedence over `TENKI_API_KEY`** when both are set. |
121
+ | `TENKI_API_ENDPOINT` | `https://api.tenki.cloud` | Control-plane base URL (`TENKI_API_URL` is an alias). |
122
+ | `TENKI_MCP_READONLY` | off | `1` registers only read tools (no create/run/delete/spend). |
123
+ | `TENKI_MCP_DISABLED_TOOLS` | — | Comma-separated tool names to skip registering. |
124
+ | `TENKI_MCP_AUDIT` | off | `1` logs each tool call name + arg keys to stderr. |
125
+ | `TENKI_MCP_TRANSPORT` | `stdio` | `http` serves Streamable HTTP instead (see below). |
126
+ | `PORT` | `3000` | HTTP transport port. |
127
+ | `TENKI_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind host; non-loopback requires `TENKI_MCP_HTTP_TOKEN`. |
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. |
135
+
136
+ ## Tools
137
+
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:
139
+
140
+ | Domain | Tools |
141
+ |---|---|
142
+ | **Auth** | `tenki_auth_status` (which credential is configured, and does it work — the only tool available when none is) |
143
+ | **Identity** | `tenki_whoami` |
144
+ | **Run** | `tenki_run_code` (one-shot: boot → run shell/python/js → tear down) |
145
+ | **Sandboxes** | `tenki_create_sandbox` · `tenki_get_sandbox` · `tenki_list_sandboxes` · `tenki_terminate_sandbox` · `tenki_pause_sandbox` · `tenki_resume_sandbox` |
146
+ | **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes` |
147
+ | **Exec** | `tenki_exec` (stdout/stderr/exit inline) |
148
+ | **Files** | `tenki_read_file` · `tenki_write_file` · `tenki_list_files` · `tenki_stat_path` · `tenki_make_dir` · `tenki_remove_path` · `tenki_move_path` |
149
+ | **Git** | `tenki_git` (clone/checkout/diff/log — the API supports exactly these four; run other git commands via `tenki_exec`) |
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 |
151
+ | **Artifacts** (binary transfer) | `tenki_get_upload_url` · `tenki_get_download_url` (signed URLs for binary PUT/GET) |
152
+ | **SSH** | `tenki_update_ssh_keys` · `tenki_issue_ssh_cert` · `tenki_list_ssh_gateways` |
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 |
156
+ | **Workspace** | `tenki_get_workspace_usage` · `tenki_get_workspace_settings` · `tenki_update_workspace_settings` · `tenki_get_snapshot_retention_settings` · `tenki_update_snapshot_retention_settings` |
157
+
158
+ Full per-release breakdown in [CHANGELOG.md](CHANGELOG.md).
159
+
160
+ ## Auth
161
+
162
+ Set one of `TENKI_API_KEY` or `TENKI_AUTH_TOKEN` — when both are set, `TENKI_AUTH_TOKEN` wins. The header is chosen by token prefix: `tk_…` → `Authorization: Bearer`, `ory_st_…` → `X-Session-Token`, otherwise a session cookie. Override the endpoint with `TENKI_API_ENDPOINT` (default `https://api.tenki.cloud`).
163
+
164
+ **Without a credential the server still starts**, registering only `tenki_auth_status` — so instead of an MCP client reporting an opaque "server failed to start", the agent can call that tool and get told what to set. Ask it "check tenki auth status" any time other tools return auth errors: it reports the credential kind (API key vs session token), the endpoint, and whether a live identity probe succeeded — never the token itself. It reports status only; get a credential with `tenki login` or from the dashboard.
165
+
166
+ ## Host it over HTTP (v2.0-beta)
167
+
168
+ Besides stdio, the server speaks **Streamable HTTP** so it can be hosted for remote MCP clients:
169
+
170
+ ```bash
171
+ TENKI_MCP_TRANSPORT=http PORT=3000 TENKI_API_KEY=… npx -y @tenkicloud/mcp
172
+ # → tenki-mcp running on http://127.0.0.1:3000/mcp (Streamable HTTP) [loopback only, no auth]
173
+ ```
174
+
175
+ For a single-user deployment, HTTP mode can hold one shared `TENKI_API_KEY` and therefore exposes a powerful capability. By default it:
176
+
177
+ - **binds to loopback (`127.0.0.1`) only** — set `TENKI_MCP_HTTP_HOST=0.0.0.0` to expose it, but then
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.
179
+ - **DNS-rebinding protection** is on (Host-header allowlist), so a malicious web page can't drive your local server.
180
+
181
+ ```bash
182
+ # expose to a network safely:
183
+ TENKI_MCP_TRANSPORT=http TENKI_MCP_HTTP_HOST=0.0.0.0 PORT=3000 \
184
+ TENKI_MCP_HTTP_TOKEN=$(openssl rand -hex 32) TENKI_API_KEY=… npx -y @tenkicloud/mcp
185
+ ```
186
+
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.
190
+
191
+ ## How it works
192
+
193
+ Tenki's API is **ConnectRPC** — JSON over HTTP/1.1, not REST. Every control-plane call is `POST https://api.tenki.cloud/tenki.sandbox.v1.SandboxService/{Method}` with a lowerCamelCase JSON body. Per-session file I/O runs on a **separate data-plane endpoint** returned at create time, authenticated with a short-lived session certificate. This server owns both transports so the tools stay one-liners.
194
+
195
+ Command output: `tenki_exec` and `tenki_run_code` capture `stdout`/`stderr` by redirecting to files (`sh -c '… > out 2> err'`) and reading them back over the data plane, so you get the output inline through a plain HTTP client.
196
+
197
+ The wire details are ported from the live-verified [n8n community node](https://github.com/opencolin/n8n-nodes-tenki).
198
+
199
+ ## Security
200
+
201
+ This server holds a Tenki API key and can run code + spend credits, so treat it as a capability. Full model + [CSA MCP Server Top-10](https://modelcontextprotocol-security.io/top10/server/) mapping in **[SECURITY.md](SECURITY.md)**. Quick controls:
202
+
203
+ - **Least privilege:** every tool carries MCP annotations (`readOnlyHint` / `destructiveHint`). Run `TENKI_MCP_READONLY=1` for an inspection-only server (read tools only), or `TENKI_MCP_DISABLED_TOOLS=tenki_run_code,…` to drop specific tools.
204
+ - **HTTP transport** is loopback-only by default and requires a bearer token to expose to a network (see [Host it over HTTP](#host-it-over-http-v20-beta)).
205
+ - **Audit:** `TENKI_MCP_AUDIT=1` logs each tool call's name to stderr.
206
+ - **Untrusted output:** `tenki_run_code`/`tenki_exec`/`tenki_read_file` return output from untrusted code — clients should treat tool results as data, not instructions.
207
+
208
+ Report vulnerabilities via a private [security advisory](https://github.com/LuxorLabs/tenki-mcp/security/advisories/new), not a public issue.
209
+
210
+ ## Related
211
+
212
+ - **Tenki Sandbox** — the platform: https://tenki.cloud
213
+ - **n8n-nodes-tenki** — Tenki as an n8n node: https://github.com/opencolin/n8n-nodes-tenki
214
+
215
+ ## License
216
+
217
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ # Security
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 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
+
5
+ ## Reporting a vulnerability
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
+
8
+ ## Trust boundaries (read this first)
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
+ - **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 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
+
13
+ ## Controls this server provides
14
+
15
+ ### Least privilege (MCP-07 — the main lever)
16
+ Every tool is tagged with MCP **annotations** so clients can surface/gate danger:
17
+ `readOnlyHint` on inspection tools, `destructiveHint` on the 13 that delete/terminate/revoke, `openWorldHint` on all (they call an external API). Plus two env controls:
18
+
19
+ | Env | Effect |
20
+ |---|---|
21
+ | `TENKI_MCP_READONLY=1` | Register **only read-only tools** — no create, run, spend, or delete. For inspection-only deployments. |
22
+ | `TENKI_MCP_DISABLED_TOOLS=a,b,c` | Skip the named tools (e.g. disable `tenki_run_code` where code execution isn't wanted). |
23
+
24
+ Grant the smallest set that the use case needs.
25
+
26
+ ### Transport security (MCP-05, MCP-10)
27
+ - **stdio** (default) has no network surface.
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
+
30
+ ### Secrets & audit (MCP-04, Observability)
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
+ - `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
+
34
+ ## CSA MCP Server Top-10 mapping
35
+
36
+ | # | Risk | tenki-mcp posture |
37
+ |---|---|---|
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 | 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
+ | 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 | 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
+ | MCP-05 | Insecure Configuration | HTTP transport is loopback + token + DNS-rebinding-protected + DoS-capped by default |
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
+ | MCP-07 | Excessive Permissions | tool annotations + `TENKI_MCP_READONLY` + `TENKI_MCP_DISABLED_TOOLS` |
45
+ | MCP-08 | Data Exfiltration | microVM isolation; sandbox **outbound networking is off unless `allow_outbound` is set**; the server itself stores/forwards nothing |
46
+ | MCP-09 | Context Spoofing | tool results are raw API/sandbox output surfaced as data, not merged into instructions |
47
+ | MCP-10 | Insecure Communication | control plane is HTTPS; run the HTTP transport behind TLS for any non-loopback use |
48
+
49
+ ## Cost / resource notes
50
+ `tenki_run_code` is cost-guarded (1 vCPU, 1 GB, 10-min cap, 5-min idle) and self-terminates. `tenki_create_sandbox` and other create tools **spend real credits** — bound them with your **Tenki workspace limits**, `TENKI_MCP_READONLY`/denylist, and `TENKI_MCP_AUDIT` for visibility.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * TenkiClient — a dependency-free client for the Tenki Cloud API.
3
+ *
4
+ * Tenki's API is ConnectRPC (JSON over HTTP/1.1), not REST. Every control-plane
5
+ * call is `POST {baseUrl}/tenki.sandbox.v1.SandboxService/{Method}` with a
6
+ * lowerCamelCase JSON body and a JSON response. Per-session file I/O runs on a
7
+ * separate data-plane endpoint returned at session-create time, authenticated
8
+ * with a short-lived session certificate.
9
+ *
10
+ * The wire details here (headers, the control/data-plane split, and capturing
11
+ * command stdout/stderr via an `sh -c` redirect + data-plane ReadFile) are
12
+ * ported from the live-verified n8n community node
13
+ * (github.com/opencolin/n8n-nodes-tenki).
14
+ */
15
+ export type Language = "shell" | "python" | "javascript";
16
+ export interface ExecResult {
17
+ command: string;
18
+ args: string[];
19
+ stdout: string;
20
+ stderr: string;
21
+ exitCode: number;
22
+ ok: boolean;
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;
30
+ }
31
+ export interface TenkiClientOptions {
32
+ /** Timeout for unary control/data-plane calls (default 30s). */
33
+ timeoutMs?: number;
34
+ /** Timeout for ExecuteCommand when the command itself has no timeout (default 630s). */
35
+ execTimeoutMs?: number;
36
+ /** Timeout for long storage/VM operations — see SLOW_METHOD (default 600s). */
37
+ slowTimeoutMs?: number;
38
+ /** Assumed session-credential lifetime when the API returns no parseable expiry (default 5min). */
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;
44
+ }
45
+ export declare class TenkiClient {
46
+ private readonly token;
47
+ private readonly baseUrl;
48
+ private readonly credCache;
49
+ private readonly credInflight;
50
+ private readonly timeoutMs;
51
+ private readonly execTimeoutMs;
52
+ private readonly slowTimeoutMs;
53
+ private readonly credTtlMs;
54
+ private readonly workspaceId?;
55
+ private readonly bearerTokenProvider?;
56
+ constructor(token: string, baseUrl?: string, opts?: TenkiClientOptions);
57
+ private controlAuthHeaders;
58
+ /**
59
+ * ExecuteCommand blocks until the command finishes, so its timeout follows
60
+ * the command's own timeout (plus margin) instead of the unary default.
61
+ */
62
+ private timeoutFor;
63
+ /**
64
+ * fetch + full body read under ONE timeout, with a readable error instead of
65
+ * a bare AbortError. Reading the body inside the guarded section matters: a
66
+ * response whose headers arrive but whose body stalls must fail with the
67
+ * same friendly, retryable timeout as a connection that never responds.
68
+ */
69
+ private fetchTextWithTimeout;
70
+ /**
71
+ * Retry policy: rate limits always; `unavailable` and gateway-shaped
72
+ * 502/503/504 (load balancers and CDNs return these with an HTML or empty
73
+ * body, no ConnectRPC code) for every method that cannot double-apply.
74
+ */
75
+ private shouldRetry;
76
+ /** Exponential backoff with half-jitter; a Retry-After header (seconds) wins, capped. */
77
+ private backoffMs;
78
+ /**
79
+ * Unary control-plane call. The WHOLE call — every attempt plus every
80
+ * backoff sleep — draws from one shared deadline (the method's timeout), so
81
+ * retries can never stack past it: an MCP client's own 60s default would
82
+ * otherwise give up before our per-attempt timeouts did. Rate-limit
83
+ * rejections retry with jittered backoff (honoring Retry-After); transient
84
+ * `unavailable`/gateway errors AND transport-level failures (timeout,
85
+ * connection reset, DNS) retry for every method that cannot double-apply.
86
+ * `service` defaults to SandboxService; pass another fully-qualified ConnectRPC
87
+ * service (e.g. the SSH gateway service) for methods hosted elsewhere.
88
+ */
89
+ control(method: string, body?: Record<string, unknown>, service?: string): Promise<Record<string, any>>;
90
+ /**
91
+ * Mint (and cache per session) the data-plane endpoint + session certificate.
92
+ * A credential with no parseable expiry is cached for credTtlMs — never
93
+ * forever — and a known expiry is shortened by a skew so an in-flight call
94
+ * can't straddle the real expiry. Minting is single-flight: concurrent data
95
+ * calls on a cold cache share one mint instead of each paying a
96
+ * control-plane round trip for its own certificate.
97
+ */
98
+ private credentialFor;
99
+ private mintCredential;
100
+ /**
101
+ * Unary data-plane call. The inner request is wrapped as
102
+ * `{ request: { sessionId, ...request } }`. This is the path behind every
103
+ * file tool AND how tenki_exec reads back its own output, so it gets the
104
+ * same treatment as the control plane: one shared deadline, rate limits
105
+ * retried for every method, transient/gateway/transport failures retried
106
+ * for read-shaped methods. A stale-certificate failure (401/unauthenticated
107
+ * — the cert may have expired server-side regardless of its stated expiry)
108
+ * invalidates the cache and re-mints once; permission_denied is NOT an auth
109
+ * failure — a denied path is a normal user error a fresh cert can never
110
+ * fix, so re-minting would waste a control-plane round trip per denied call.
111
+ */
112
+ data(sessionId: string, method: string, request?: Record<string, unknown>): Promise<Record<string, any>>;
113
+ /**
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.)
118
+ *
119
+ * CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
120
+ * non-empty owner_id, but derives the real owner from the authenticated
121
+ * identity server-side. WhoAmI can return other owner types (e.g. WORKSPACE
122
+ * for workspace-scoped keys), which the validator rejects — so for those we
123
+ * send the same placeholder the first-party SDKs hardcode ("SERVICE"/"self").
124
+ */
125
+ resolveOwner(): Promise<{
126
+ ownerType?: string;
127
+ ownerId?: string;
128
+ workspaceId?: string;
129
+ }>;
130
+ /** Poll GetSession until it reaches (or passes into) the target state. */
131
+ waitForState(sessionId: string, target?: string, { timeoutMs, intervalMs }?: {
132
+ timeoutMs?: number;
133
+ intervalMs?: number;
134
+ }): Promise<Record<string, any>>;
135
+ readTextFile(sessionId: string, path: string): Promise<string>;
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;
146
+ /**
147
+ * Run a command in a session and return stdout/stderr inline.
148
+ *
149
+ * We wrap the command in `sh -c '<cmd> > out 2> err'`, then read the capture
150
+ * files back over the data plane, so output is available through a plain-HTTP
151
+ * client. Capture-read failures degrade gracefully into `captureError` rather
152
+ * than losing the run — but the result is marked NOT ok, because empty
153
+ * stdout/stderr next to a zero exit code would otherwise read as a clean,
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`.
157
+ */
158
+ execCaptured(sessionId: string, command: string, opts?: {
159
+ args?: string[];
160
+ cwd?: string;
161
+ env?: Record<string, string>;
162
+ timeoutSeconds?: number;
163
+ maxOutputBytes?: number;
164
+ }): Promise<ExecResult>;
165
+ /**
166
+ * One-shot: boot a throwaway sandbox, run code (shell / python / javascript),
167
+ * return its output, and terminate the sandbox. Cost-guarded (1 vCPU, 1 GB,
168
+ * 10-min cap, 5-min idle) so an ephemeral run can never leak a billing session.
169
+ */
170
+ runCode(language: Language, code: string, opts?: {
171
+ env?: Record<string, string>;
172
+ timeoutSeconds?: number;
173
+ }): Promise<ExecResult & {
174
+ sessionId: string;
175
+ language: Language;
176
+ }>;
177
+ }