Skip to content

Commit 426c26a

Browse files
feat(mcp): operate tools — drive the full bundle lifecycle (vault/env/presign/pause/rotate/wake) (#41)
* feat(mcp): add get_capabilities + get_deployment_events read-only tools Closes the P0 "agent flies blind" gap: the MCP had 19 tools covering provision + deploy-create but no way to (a) read the live tier matrix before a call 402s, or (b) autopsy a failed deploy to self-correct. - get_capabilities → GET /api/v1/capabilities (auth-OPTIONAL public discovery surface; iterates the api's live plans registry, renders per-tier storage/connection/resource-count/deployment caps + pricing + backup/RPO/RTO promises, cheapest-first). - get_deployment_events → GET /api/v1/deployments/:id/events (auth-required; the rule-27 failure-timeline: kind/reason/exit_code/event/last_lines/hint/ created_at, newest-first, so an agent can fix a broken Dockerfile and redeploy). Follows the existing tool pattern: client methods in src/client.ts reuse request<T>() so the error envelope (agent_action/upgrade_url/claim_url) is preserved verbatim through ApiError → formatError; tools registered in src/index.ts format output like the sibling deploy tools. Tests: mock-api gains a public /capabilities route and a /deployments/:id/ events route (matched before the generic deployments-prefix block; seeds a failure_autopsy timeline for "fail"-named deploys). Drift guards updated 19→21 (tool-coverage J20/J21, integration EXPECTED_TOOLS). New client-unit, tools-unit, and tool-contract tests cover success/empty/limit/404/auth-gate + every fallback branch. README Tools table updated. Gate: npm run build && npm test — 413 pass / 0 fail; client.js 100% line / 95.76% branch, index.js 99.81% line / 95.62% branch (only the pre-existing server.connect binary block uncovered), all files 95.68% branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(mcp): add operate tools so an agent can drive the full bundle lifecycle Extends the capabilities/deploy-events branch with 9 "operate" tools wrapping existing api endpoints (each route verified against api/internal/router/router.go before wiring). Preserves the ApiError → agent_action/upgrade_url passthrough and mirrors the existing tool/client pattern. New tools (J22-J30): - set_vault_key → PUT /api/v1/vault/:env/:key (closes D4: lets an agent WRITE the vault://env/KEY refs create_deploy advertises) - rotate_vault_key → POST /api/v1/vault/:env/:key/rotate - update_deploy_env → PATCH /deploy/:id/env (note: real route is /deploy/:id/env, NOT /api/v1/deployments/:id/env as the brief stated) - update_stack_env → PATCH /stacks/:slug/env - presign_storage → POST /storage/:token/presign (token-in-path broker auth) - pause_resource → POST /api/v1/resources/:id/pause (Pro+) - resume_resource → POST /api/v1/resources/:id/resume (Pro+) - rotate_credentials → POST /api/v1/resources/:id/rotate-credentials - wake_deployment → POST /deploy/:id/wake (flag-gated; 501 when scale-to-zero off) Tool descriptions stay accurate vs api/plans.yaml (vault tiers, Pro+ pause/resume, deployments_apps caps) with no hardcoded drift. Tests: - mock-api: 9 new routes + vault/stackEnv stores + seedResource/seedDeployment seams - operate-tools-unit.test.ts: success + 401/402/404/409/501/zod-validation per tool - tool-contract.test.ts: J22-J30 agent-facing error-mapping contract - prod-cohort.test.ts: live PROD integration — mints a real is_test_cohort account (POST /internal/e2e/account), drives get_capabilities + set_vault_key/rotate_vault_key against https://api.instanode.dev, then reaps the cohort. Gated behind INSTANODE_PROD_COHORT=1 + E2E_ACCOUNT_TOKEN; skips clean otherwise. - drift guards updated: 21 → 30 tools (tool-coverage MAPPED_TOOLS + integration EXPECTED_TOOLS). Gate: npm run build && npm test green (451 tests, 0 fail; client.js 100% lines, index.js 99.85%). Live-verified: PROD cohort run passed end-to-end and reaped clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: 100% patch coverage on operate-tool error branches The patch-coverage gate (diff-cover, 100% of changed lines) failed on src/index.ts: Missing lines 1859-1860,1906-1907,1940-1943,1958-1959, 1994-1997,2012-2013,2081-2082,2155-2156,2196-2197 (22 lines) — the catch(err)→formatError branches and the empty-env early-returns in the 9 new operate tools (set/rotate_vault_key, update_deploy/stack_env, presign_storage, resume_resource, rotate_credentials, etc). Root cause: the coverage.yml "Generate lcov mapped to TS sources" step omitted operate-tools-unit.test.js from its file list, so the operate tools' error paths were never executed during the lcov run even though operate-tools-unit.test.ts already asserts every one of those branches (401/402/404/409/501/empty-env). The main `npm test` script DID include the file; only the lcov-generation command was out of sync. Add operate-tools-unit.test.js to the lcov step (matching the package.json test-script order). diff-cover now reports src/index.ts (100%), 0 missing lines, overall 100%. Also gitignore the transient coverage/ artifact CI writes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7fca698 commit 426c26a

14 files changed

Lines changed: 2776 additions & 6 deletions

.github/workflows/coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
dist-test/test/client-unit.test.js \
5959
dist-test/test/index-unit.test.js \
6060
dist-test/test/tools-unit.test.js \
61+
dist-test/test/operate-tools-unit.test.js \
6162
dist-test/test/tool-coverage.test.js \
6263
dist-test/test/tool-contract.test.js \
6364
dist-test/test/env-regex-unit.test.js \

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
dist/
33
dist-test/
4+
coverage/

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,24 @@ to reach for this MCP, see <https://instanode.dev/agent.html>.
124124
| `get_stack` | `GET /stacks/{stack_id}` — Poll a stack's per-service status + URLs. Anonymous-friendly. `stack_id` required. |
125125
| `list_deployments`| `GET /api/v1/deployments` — List all deployments on the caller's team. Requires `INSTANODE_TOKEN`. |
126126
| `get_deployment` | `GET /api/v1/deployments/:id` — Fetch one deployment (poll until `status="running"`). Requires `INSTANODE_TOKEN`. |
127+
| `get_deployment_events` | `GET /api/v1/deployments/:id/events` — Read the failure-timeline autopsy for a deployment (`kind`/`reason`/`exit_code`/`event`/`last_lines`/`hint`/`created_at`, newest first) so an agent can self-correct a broken Dockerfile. Optional `limit`. Requires `INSTANODE_TOKEN`. |
127128
| `redeploy` | `POST /deploy/:id/redeploy` — Push updated code to an existing deployment BY ID. Same URL, new build. Requires `tarball_base64` (same shape as `create_deploy`) — the api never reuses the original tarball. For the more common "update by name" path prefer `create_deploy({ name, redeploy: true, tarball_base64 })`. Requires `INSTANODE_TOKEN`. |
128129
| `delete_deployment` | `DELETE /deploy/:id` — Tear down a running deployment. Irreversible. Requires `INSTANODE_TOKEN`. |
129130
| `claim_resource` | Helper — turn an `upgrade_jwt` from any `create_*` response into the dashboard claim URL the user should click. No API call. No auth required. |
130131
| `claim_token` | `POST /claim` — Programmatic claim: attach an anonymous resource to the authenticated account using its `upgrade_jwt` + `email`. No auth required. |
131132
| `list_resources` | `GET /api/v1/resources` — List resources on the caller's account. Requires `INSTANODE_TOKEN`. |
132133
| `delete_resource` | `DELETE /api/v1/resources/{token}` — Hard-delete a resource you own. Paid tier only. Requires `INSTANODE_TOKEN`. |
133134
| `get_api_token` | `POST /api/v1/auth/api-keys` — Mint a fresh bearer Personal Access Token (PAT). Requires an existing user-session `INSTANODE_TOKEN` (PATs cannot mint other PATs — the API returns 403 in that case). |
135+
| `get_capabilities`| `GET /api/v1/capabilities` — Read the live per-tier capability matrix (storage / connection / resource-count / deployment caps, pricing, backup + RPO/RTO promises) in upgrade order so an agent can plan a provision before a call `402`s. **Auth optional** (public discovery surface). |
136+
| `set_vault_key` | `PUT /api/v1/vault/{env}/{key}` — Write a secret to the team vault (always a new version). Reference it from a deploy as `vault://{env}/{key}` in `env_vars`; the API decrypts it at deploy time. Vault is paid (Hobby+ = 20 entries, Pro/Team = unlimited; Hobby/Pro restrict env to `production`). Requires `INSTANODE_TOKEN`. |
137+
| `rotate_vault_key`| `POST /api/v1/vault/{env}/{key}/rotate` — Rotate a vault secret's value (new version, recorded under a distinct audit action). Redeploy referencing apps to apply. Requires `INSTANODE_TOKEN`. |
138+
| `update_deploy_env` | `PATCH /deploy/{id}/env` — Merge env vars into an existing deployment (incoming wins; values may be `vault://env/KEY` refs). Returns the merged map with secrets redacted. Redeploy to apply. Requires `INSTANODE_TOKEN`. |
139+
| `update_stack_env`| `PATCH /stacks/{slug}/env` — Merge env vars into an existing stack (row-locked; an empty-string value deletes a key). Redeploy the stack to apply. Requires `INSTANODE_TOKEN`. |
140+
| `presign_storage` | `POST /storage/{token}/presign` — Mint a short-lived (≤1h) presigned S3 URL (`GET`/`PUT`/`HEAD`) scoped to a storage prefix. Auth is the storage token in the path — works for anonymous-tier storage. `DELETE` is not offered (a leaked URL must not wipe a prefix). |
141+
| `pause_resource` | `POST /api/v1/resources/{id}/pause` — Suspend a resource without deleting it (storage + connection URL preserved; new connections refused). **Pro tier or higher.** Requires `INSTANODE_TOKEN`. |
142+
| `resume_resource` | `POST /api/v1/resources/{id}/resume` — Un-pause a resource (same connection URL keeps working). **Pro tier or higher.** Requires `INSTANODE_TOKEN`. |
143+
| `rotate_credentials` | `POST /api/v1/resources/{id}/rotate-credentials` — Rotate a resource's password; returns the NEW `connection_url` in plaintext (host + DB unchanged). Locks out a leaked old URL. Requires `INSTANODE_TOKEN`. |
144+
| `wake_deployment` | `POST /deploy/{id}/wake` — Explicitly wake a scaled-to-zero deployment (scales to 1 replica; cold-start before serving). Flag-gated on the platform: returns 501 `scale_to_zero_disabled` when the feature is off. Requires `INSTANODE_TOKEN`. |
134145

135146
### Container deployment (`create_deploy`)
136147

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
"dev": "tsc --watch",
4747
"start": "node dist/index.js",
4848
"pretest": "tsc && tsc -p tsconfig.test.json",
49-
"test": "node --test --experimental-test-coverage --test-coverage-exclude='dist-test/test/**' --test-coverage-exclude='dist/**' --test-coverage-exclude='node_modules/**' dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js dist-test/test/tool-coverage.test.js dist-test/test/tool-contract.test.js dist-test/test/env-regex-unit.test.js dist-test/test/input-hardening-unit.test.js",
50-
"test:nocov": "node --test dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js dist-test/test/tool-coverage.test.js dist-test/test/tool-contract.test.js dist-test/test/env-regex-unit.test.js dist-test/test/input-hardening-unit.test.js",
49+
"test": "node --test --experimental-test-coverage --test-coverage-exclude='dist-test/test/**' --test-coverage-exclude='dist/**' --test-coverage-exclude='node_modules/**' dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/prod-cohort.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js dist-test/test/operate-tools-unit.test.js dist-test/test/tool-coverage.test.js dist-test/test/tool-contract.test.js dist-test/test/env-regex-unit.test.js dist-test/test/input-hardening-unit.test.js",
50+
"test:nocov": "node --test dist-test/test/integration.test.js dist-test/test/live-smoke.test.js dist-test/test/prod-cohort.test.js dist-test/test/client-unit.test.js dist-test/test/index-unit.test.js dist-test/test/tools-unit.test.js dist-test/test/operate-tools-unit.test.js dist-test/test/tool-coverage.test.js dist-test/test/tool-contract.test.js dist-test/test/env-regex-unit.test.js dist-test/test/input-hardening-unit.test.js",
5151
"test:smoke": "bash test.sh",
5252
"prepublishOnly": "npm run build"
5353
},

0 commit comments

Comments
 (0)