@tenkicloud/mcp 0.1.0 → 0.2.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 +5 -8
- package/dist/client.d.ts +23 -5
- package/dist/client.js +76 -11
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -4
- package/dist/tools/auth_status.js +1 -1
- 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,8 +20,6 @@ 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
|
|
|
27
25
|
```bash
|
|
@@ -111,7 +109,7 @@ Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` comman
|
|
|
111
109
|
|
|
112
110
|
## Tools
|
|
113
111
|
|
|
114
|
-
**
|
|
112
|
+
**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
113
|
|
|
116
114
|
| Domain | Tools |
|
|
117
115
|
|---|---|
|
|
@@ -119,17 +117,16 @@ Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` comman
|
|
|
119
117
|
| **Identity** | `tenki_whoami` |
|
|
120
118
|
| **Run** | `tenki_run_code` (one-shot: boot → run shell/python/js → tear down) |
|
|
121
119
|
| **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`
|
|
120
|
+
| **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes` |
|
|
123
121
|
| **Exec** | `tenki_exec` (stdout/stderr/exit inline) |
|
|
124
122
|
| **Files** | `tenki_read_file` · `tenki_write_file` · `tenki_list_files` · `tenki_stat_path` · `tenki_make_dir` · `tenki_remove_path` · `tenki_move_path` |
|
|
125
123
|
| **Git** | `tenki_git` (clone/checkout/diff/log — the API supports exactly these four; run other git commands via `tenki_exec`) |
|
|
126
124
|
| **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
125
|
| **Artifacts** (binary transfer) | `tenki_get_upload_url` · `tenki_get_download_url` (signed URLs for binary PUT/GET) |
|
|
128
126
|
| **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 |
|
|
127
|
+
| **Snapshots** | create · get · list · list-session · list-workspace · list-dangling · update · delete · get-download-url |
|
|
128
|
+
| **Volumes** | create · get · list · update · delete · resize · attach · detach |
|
|
129
|
+
| **Templates** | create · get · list · update · delete · build · cancel-build · get-build · list-active-builds |
|
|
133
130
|
| **Workspace** | `tenki_get_workspace_usage` · `tenki_get_workspace_settings` · `tenki_update_workspace_settings` · `tenki_get_snapshot_retention_settings` · `tenki_update_snapshot_retention_settings` |
|
|
134
131
|
|
|
135
132
|
Full per-release breakdown in [CHANGELOG.md](CHANGELOG.md).
|
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). */
|
|
@@ -98,9 +104,10 @@ export declare class TenkiClient {
|
|
|
98
104
|
*/
|
|
99
105
|
data(sessionId: string, method: string, request?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
100
106
|
/**
|
|
101
|
-
* Resolve the calling identity + a default workspace
|
|
102
|
-
*
|
|
103
|
-
*
|
|
107
|
+
* Resolve the calling identity + a default workspace for CreateSession and
|
|
108
|
+
* other workspace-scoped calls. (Projects were removed from the API — the
|
|
109
|
+
* proto reserves every project_id field and WhoAmI no longer lists projects —
|
|
110
|
+
* so there is nothing narrower than a workspace to resolve.)
|
|
104
111
|
*
|
|
105
112
|
* CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
|
|
106
113
|
* non-empty owner_id, but derives the real owner from the authenticated
|
|
@@ -112,7 +119,6 @@ export declare class TenkiClient {
|
|
|
112
119
|
ownerType?: string;
|
|
113
120
|
ownerId?: string;
|
|
114
121
|
workspaceId?: string;
|
|
115
|
-
projectId?: string;
|
|
116
122
|
}>;
|
|
117
123
|
/** Poll GetSession until it reaches (or passes into) the target state. */
|
|
118
124
|
waitForState(sessionId: string, target?: string, { timeoutMs, intervalMs }?: {
|
|
@@ -121,6 +127,15 @@ export declare class TenkiClient {
|
|
|
121
127
|
}): Promise<Record<string, any>>;
|
|
122
128
|
readTextFile(sessionId: string, path: string): Promise<string>;
|
|
123
129
|
writeTextFile(sessionId: string, path: string, text: string): Promise<Record<string, any>>;
|
|
130
|
+
/**
|
|
131
|
+
* Read a capture file back, capping what crosses the wire. Stat first: a
|
|
132
|
+
* file at or under the cap is read whole; a larger one gets a server-side
|
|
133
|
+
* head+tail preview (assembled by `sh` in the sandbox, so the full body
|
|
134
|
+
* never leaves it — ReadFile has no range support). The preview file is the
|
|
135
|
+
* caller's to clean up (returned as previewPath); the ORIGINAL is theirs to
|
|
136
|
+
* keep, so the user can page through the full output afterwards.
|
|
137
|
+
*/
|
|
138
|
+
private readCaptureFile;
|
|
124
139
|
/**
|
|
125
140
|
* Run a command in a session and return stdout/stderr inline.
|
|
126
141
|
*
|
|
@@ -129,13 +144,16 @@ export declare class TenkiClient {
|
|
|
129
144
|
* client. Capture-read failures degrade gracefully into `captureError` rather
|
|
130
145
|
* than losing the run — but the result is marked NOT ok, because empty
|
|
131
146
|
* stdout/stderr next to a zero exit code would otherwise read as a clean,
|
|
132
|
-
* silent success.
|
|
147
|
+
* silent success. Streams over maxOutputBytes come back truncated
|
|
148
|
+
* (head+tail) with the full capture file retained in the sandbox — see
|
|
149
|
+
* readCaptureFile; truncation does not affect `ok`.
|
|
133
150
|
*/
|
|
134
151
|
execCaptured(sessionId: string, command: string, opts?: {
|
|
135
152
|
args?: string[];
|
|
136
153
|
cwd?: string;
|
|
137
154
|
env?: Record<string, string>;
|
|
138
155
|
timeoutSeconds?: number;
|
|
156
|
+
maxOutputBytes?: number;
|
|
139
157
|
}): Promise<ExecResult>;
|
|
140
158
|
/**
|
|
141
159
|
* One-shot: boot a throwaway sandbox, run code (shell / python / javascript),
|
package/dist/client.js
CHANGED
|
@@ -71,6 +71,17 @@ const CRED_EXPIRY_SKEW_MS = 30_000;
|
|
|
71
71
|
const MIN_CRED_TTL_MS = 5_000; // floor for the skewed expiry of a short-lived credential
|
|
72
72
|
/** Home directory of the sandbox's `tenki` user; run-code scripts and capture files live here. */
|
|
73
73
|
const SANDBOX_HOME = "/home/tenki";
|
|
74
|
+
// Inline-output budget per stream (stdout / stderr), in bytes. Outputs at or
|
|
75
|
+
// under the cap return whole; larger ones return a head+tail preview and the
|
|
76
|
+
// full capture file is KEPT in the sandbox so the caller can page through it.
|
|
77
|
+
// The cap exists for the consumer (an LLM context) and for this process: the
|
|
78
|
+
// data plane's ReadFile has no range support, so the only alternative to a
|
|
79
|
+
// server-side preview would be pulling the entire body over the wire.
|
|
80
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 65_536;
|
|
81
|
+
const MIN_OUTPUT_BYTES = 1_024;
|
|
82
|
+
// Head-heavy split: the start of a log names the command/failure context; the
|
|
83
|
+
// tail carries the final error. 3:1 mirrors how people read build output.
|
|
84
|
+
const TRUNCATE_HEAD_FRACTION = 0.75;
|
|
74
85
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
75
86
|
function safeJson(text) {
|
|
76
87
|
try {
|
|
@@ -335,9 +346,10 @@ export class TenkiClient {
|
|
|
335
346
|
}
|
|
336
347
|
}
|
|
337
348
|
/**
|
|
338
|
-
* Resolve the calling identity + a default workspace
|
|
339
|
-
*
|
|
340
|
-
*
|
|
349
|
+
* Resolve the calling identity + a default workspace for CreateSession and
|
|
350
|
+
* other workspace-scoped calls. (Projects were removed from the API — the
|
|
351
|
+
* proto reserves every project_id field and WhoAmI no longer lists projects —
|
|
352
|
+
* so there is nothing narrower than a workspace to resolve.)
|
|
341
353
|
*
|
|
342
354
|
* CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
|
|
343
355
|
* non-empty owner_id, but derives the real owner from the authenticated
|
|
@@ -348,8 +360,7 @@ export class TenkiClient {
|
|
|
348
360
|
async resolveOwner() {
|
|
349
361
|
const resp = await this.control("WhoAmI", {});
|
|
350
362
|
const workspaces = Array.isArray(resp.workspaces) ? resp.workspaces : [];
|
|
351
|
-
const ws = workspaces
|
|
352
|
-
const proj = Array.isArray(ws?.projects) ? ws.projects[0] : undefined;
|
|
363
|
+
const ws = workspaces[0];
|
|
353
364
|
let ownerType = resp.ownerType;
|
|
354
365
|
let ownerId = resp.ownerId;
|
|
355
366
|
// Substitute the placeholder only when WhoAmI returned a type CreateSession
|
|
@@ -363,7 +374,6 @@ export class TenkiClient {
|
|
|
363
374
|
ownerType,
|
|
364
375
|
ownerId,
|
|
365
376
|
workspaceId: ws?.workspaceId ?? ws?.id,
|
|
366
|
-
projectId: proj?.projectId ?? proj?.id,
|
|
367
377
|
};
|
|
368
378
|
}
|
|
369
379
|
/** Poll GetSession until it reaches (or passes into) the target state. */
|
|
@@ -393,6 +403,37 @@ export class TenkiClient {
|
|
|
393
403
|
const content = Buffer.from(text, "utf8").toString("base64");
|
|
394
404
|
return this.data(sessionId, "WriteFile", { path, content });
|
|
395
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Read a capture file back, capping what crosses the wire. Stat first: a
|
|
408
|
+
* file at or under the cap is read whole; a larger one gets a server-side
|
|
409
|
+
* head+tail preview (assembled by `sh` in the sandbox, so the full body
|
|
410
|
+
* never leaves it — ReadFile has no range support). The preview file is the
|
|
411
|
+
* caller's to clean up (returned as previewPath); the ORIGINAL is theirs to
|
|
412
|
+
* keep, so the user can page through the full output afterwards.
|
|
413
|
+
*/
|
|
414
|
+
async readCaptureFile(sessionId, path, capBytes) {
|
|
415
|
+
// Stat is an optimization (skip pulling a body we'd mostly discard), not a
|
|
416
|
+
// gate: if it fails, fall back to the plain whole read — whose own failure
|
|
417
|
+
// still surfaces as captureError in the caller.
|
|
418
|
+
let size;
|
|
419
|
+
try {
|
|
420
|
+
const stat = await this.data(sessionId, "Stat", { path });
|
|
421
|
+
size = Number(stat.size ?? 0); // int64 arrives as a JSON string
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
size = undefined;
|
|
425
|
+
}
|
|
426
|
+
if (size === undefined || !Number.isFinite(size) || size <= capBytes) {
|
|
427
|
+
return { text: await this.readTextFile(sessionId, path), truncated: false };
|
|
428
|
+
}
|
|
429
|
+
const head = Math.floor(capBytes * TRUNCATE_HEAD_FRACTION);
|
|
430
|
+
const tail = capBytes - head;
|
|
431
|
+
const previewPath = `${path}.preview`;
|
|
432
|
+
const marker = `\n[... output truncated: ${size} bytes total, showing the first ${head} and last ${tail}. Full output kept in the sandbox at ${path} ...]\n`;
|
|
433
|
+
const script = `{ head -c ${head} ${shellQuote(path)}; printf %s ${shellQuote(marker)}; tail -c ${tail} ${shellQuote(path)}; } > ${shellQuote(previewPath)}`;
|
|
434
|
+
await this.control("ExecuteCommand", { sessionId, command: "sh", args: ["-c", script] });
|
|
435
|
+
return { text: await this.readTextFile(sessionId, previewPath), truncated: true, previewPath };
|
|
436
|
+
}
|
|
396
437
|
/**
|
|
397
438
|
* Run a command in a session and return stdout/stderr inline.
|
|
398
439
|
*
|
|
@@ -401,7 +442,9 @@ export class TenkiClient {
|
|
|
401
442
|
* client. Capture-read failures degrade gracefully into `captureError` rather
|
|
402
443
|
* than losing the run — but the result is marked NOT ok, because empty
|
|
403
444
|
* stdout/stderr next to a zero exit code would otherwise read as a clean,
|
|
404
|
-
* silent success.
|
|
445
|
+
* silent success. Streams over maxOutputBytes come back truncated
|
|
446
|
+
* (head+tail) with the full capture file retained in the sandbox — see
|
|
447
|
+
* readCaptureFile; truncation does not affect `ok`.
|
|
405
448
|
*/
|
|
406
449
|
async execCaptured(sessionId, command, opts = {}) {
|
|
407
450
|
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -428,18 +471,39 @@ export class TenkiClient {
|
|
|
428
471
|
// unparseable code to -1 so the user still gets their stdout/stderr.
|
|
429
472
|
const rawExit = typeof execution.exitCode === "number" ? execution.exitCode : Number(execution.exitCode ?? 0);
|
|
430
473
|
const exitCode = Number.isFinite(rawExit) ? Math.trunc(rawExit) : -1;
|
|
474
|
+
const cap = Math.max(MIN_OUTPUT_BYTES, Math.floor(opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES));
|
|
431
475
|
let stdout = "";
|
|
432
476
|
let stderr = "";
|
|
433
477
|
let captureError;
|
|
478
|
+
let stdoutTruncated = false;
|
|
479
|
+
let stderrTruncated = false;
|
|
480
|
+
// Truncated originals are KEPT (their path is returned) so the caller can
|
|
481
|
+
// page through them; everything else — untruncated captures, previews — is
|
|
482
|
+
// removed.
|
|
483
|
+
const toRemove = [];
|
|
434
484
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
485
|
+
const [out, err] = await Promise.all([
|
|
486
|
+
this.readCaptureFile(sessionId, outPath, cap),
|
|
487
|
+
this.readCaptureFile(sessionId, errPath, cap),
|
|
488
|
+
]);
|
|
489
|
+
stdout = out.text;
|
|
490
|
+
stderr = err.text;
|
|
491
|
+
stdoutTruncated = out.truncated;
|
|
492
|
+
stderrTruncated = err.truncated;
|
|
493
|
+
if (out.previewPath)
|
|
494
|
+
toRemove.push(out.previewPath);
|
|
495
|
+
if (err.previewPath)
|
|
496
|
+
toRemove.push(err.previewPath);
|
|
437
497
|
}
|
|
438
498
|
catch (e) {
|
|
439
499
|
captureError = e.message;
|
|
440
500
|
}
|
|
501
|
+
if (!stdoutTruncated)
|
|
502
|
+
toRemove.push(outPath);
|
|
503
|
+
if (!stderrTruncated)
|
|
504
|
+
toRemove.push(errPath);
|
|
441
505
|
try {
|
|
442
|
-
await this.control("ExecuteCommand", { sessionId, command: "rm", args: ["-f",
|
|
506
|
+
await this.control("ExecuteCommand", { sessionId, command: "rm", args: ["-f", ...toRemove] });
|
|
443
507
|
}
|
|
444
508
|
catch {
|
|
445
509
|
// Session may have gone away; capture files die with it.
|
|
@@ -452,6 +516,8 @@ export class TenkiClient {
|
|
|
452
516
|
exitCode,
|
|
453
517
|
ok: exitCode === 0 && !captureError,
|
|
454
518
|
...(captureError ? { captureError } : {}),
|
|
519
|
+
...(stdoutTruncated ? { stdoutTruncated: true, stdoutPath: outPath } : {}),
|
|
520
|
+
...(stderrTruncated ? { stderrTruncated: true, stderrPath: errPath } : {}),
|
|
455
521
|
};
|
|
456
522
|
}
|
|
457
523
|
/**
|
|
@@ -469,7 +535,6 @@ export class TenkiClient {
|
|
|
469
535
|
...(owner.ownerType ? { ownerType: owner.ownerType } : {}),
|
|
470
536
|
...(owner.ownerId ? { ownerId: owner.ownerId } : {}),
|
|
471
537
|
...(owner.workspaceId ? { workspaceId: owner.workspaceId } : {}),
|
|
472
|
-
...(owner.projectId ? { projectId: owner.projectId } : {}),
|
|
473
538
|
...(opts.env && Object.keys(opts.env).length ? { env: opts.env } : {}),
|
|
474
539
|
});
|
|
475
540
|
const session = create.session ?? create;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ const token = process.env.TENKI_AUTH_TOKEN || process.env.TENKI_API_KEY;
|
|
|
26
26
|
if (!token) {
|
|
27
27
|
console.error("tenki-mcp: no credential — starting in unauthenticated mode (only tenki_auth_status is available). " +
|
|
28
28
|
"Set TENKI_API_KEY (tk_…) or TENKI_AUTH_TOKEN (ory_st_…) in the server's env and restart, " +
|
|
29
|
-
"e.g. claude mcp add tenki --env TENKI_API_KEY=tk_… -- npx -y
|
|
29
|
+
"e.g. claude mcp add tenki --env TENKI_API_KEY=tk_… -- npx -y @tenkicloud/mcp");
|
|
30
30
|
}
|
|
31
31
|
const baseUrl = process.env.TENKI_API_ENDPOINT || process.env.TENKI_API_URL || undefined;
|
|
32
32
|
/** Positive integer from env, or undefined so the client keeps its own default. */
|
package/dist/server.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
16
|
import type { TenkiClient } from "./client.js";
|
|
17
|
-
export declare const VERSION = "0.
|
|
17
|
+
export declare const VERSION = "0.2.0";
|
|
18
18
|
type Cls = "read" | "write" | "destructive";
|
|
19
19
|
export declare function classifyTool(name: string): Cls;
|
|
20
20
|
/**
|
|
@@ -24,7 +24,7 @@ export declare function classifyTool(name: string): Cls;
|
|
|
24
24
|
* start — which MCP clients report as an opaque "server failed to start" —
|
|
25
25
|
* the server boots with ONLY tenki_auth_status registered, so an agent can
|
|
26
26
|
* discover and explain the missing credential. Registering the other tools in
|
|
27
|
-
* that state would offer
|
|
27
|
+
* that state would offer 70 tools that can only fail.
|
|
28
28
|
*/
|
|
29
29
|
export declare function createServer(client: TenkiClient | null): McpServer;
|
|
30
30
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -26,12 +26,11 @@ import { registerPreviews } from "./tools/previews.js";
|
|
|
26
26
|
import { registerSnapshots } from "./tools/snapshots.js";
|
|
27
27
|
import { registerVolumes } from "./tools/volumes.js";
|
|
28
28
|
import { registerTemplates } from "./tools/templates.js";
|
|
29
|
-
import { registerRegistry } from "./tools/registry.js";
|
|
30
29
|
import { registerWorkspace } from "./tools/workspace.js";
|
|
31
30
|
import { registerArtifacts } from "./tools/artifacts.js";
|
|
32
31
|
import { registerSsh } from "./tools/ssh.js";
|
|
33
32
|
import { registerAuthStatus } from "./tools/auth_status.js";
|
|
34
|
-
export const VERSION = "0.
|
|
33
|
+
export const VERSION = "0.2.0";
|
|
35
34
|
const modules = [
|
|
36
35
|
registerIdentity,
|
|
37
36
|
registerRun,
|
|
@@ -46,7 +45,6 @@ const modules = [
|
|
|
46
45
|
registerSnapshots,
|
|
47
46
|
registerVolumes,
|
|
48
47
|
registerTemplates,
|
|
49
|
-
registerRegistry,
|
|
50
48
|
registerWorkspace,
|
|
51
49
|
registerArtifacts,
|
|
52
50
|
registerSsh,
|
|
@@ -181,7 +179,7 @@ function guard(server, opts) {
|
|
|
181
179
|
* start — which MCP clients report as an opaque "server failed to start" —
|
|
182
180
|
* the server boots with ONLY tenki_auth_status registered, so an agent can
|
|
183
181
|
* discover and explain the missing credential. Registering the other tools in
|
|
184
|
-
* that state would offer
|
|
182
|
+
* that state would offer 70 tools that can only fail.
|
|
185
183
|
*/
|
|
186
184
|
export function createServer(client) {
|
|
187
185
|
const server = new McpServer({ name: "tenki", version: VERSION });
|
|
@@ -19,7 +19,7 @@ export function describeCredential(env = process.env) {
|
|
|
19
19
|
return { kind: "session_cookie", source };
|
|
20
20
|
}
|
|
21
21
|
const CREDENTIAL_HELP = {
|
|
22
|
-
none: "No credential found. Set TENKI_API_KEY (a tk_… API key) or TENKI_AUTH_TOKEN (an ory_st_… session token) in the server's environment, then restart it — MCP clients pass env through their server config, e.g. `claude mcp add tenki --env TENKI_API_KEY=tk_… -- npx -y
|
|
22
|
+
none: "No credential found. Set TENKI_API_KEY (a tk_… API key) or TENKI_AUTH_TOKEN (an ory_st_… session token) in the server's environment, then restart it — MCP clients pass env through their server config, e.g. `claude mcp add tenki --env TENKI_API_KEY=tk_… -- npx -y @tenkicloud/mcp`, or the \"env\" block in claude_desktop_config.json / .cursor/mcp.json.",
|
|
23
23
|
api_key: "Authenticated with a tk_… API key (Authorization: Bearer).",
|
|
24
24
|
oauth_session_token: "Authenticated with an ory_st_… session token (X-Session-Token). Session tokens expire; an API key is the stabler choice for a long-running server.",
|
|
25
25
|
session_cookie: "Authenticated with a session cookie (the token matched neither the tk_ nor ory_st_ prefix, so it is sent as a tenki_session cookie). If that is not what you intended, check the value.",
|
package/dist/tools/common.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Keep registry-backed implementation fields out of public MCP responses. */
|
|
3
|
+
export declare function publicValue(value: unknown): unknown;
|
|
2
4
|
/** Serialize any tool return value as MCP text content. */
|
|
3
5
|
export declare const ok: (value: unknown) => {
|
|
4
6
|
content: {
|
|
@@ -21,7 +23,7 @@ export declare const portSchema: z.ZodNumber;
|
|
|
21
23
|
* leading/trailing whitespace is legal in POSIX filenames, and zod's .trim()
|
|
22
24
|
* (a transform, not a check) would silently retarget the operation to a
|
|
23
25
|
* different file. Call sites override the description with their own examples. */
|
|
24
|
-
export declare const pathSchema: z.
|
|
26
|
+
export declare const pathSchema: z.ZodString;
|
|
25
27
|
/**
|
|
26
28
|
* Preview-slug schema, matching the server's validatePreviewSlug: 3-63 chars,
|
|
27
29
|
* lowercase/digits/hyphens, no leading/trailing hyphen. ExposePort routes a
|