Skip to content

feat(vtex): read-only by default with opt-in write mode - #538

Open
guitavano wants to merge 1 commit into
mainfrom
guitavano/vtex-mcp-read-only-optin
Open

feat(vtex): read-only by default with opt-in write mode#538
guitavano wants to merge 1 commit into
mainfrom
guitavano/vtex-mcp-read-only-optin

Conversation

@guitavano

@guitavano guitavano commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

Adds a writeMode config prop to the VTEX MCP state. The MCP is now read-only by default: read tools always run, while any create/update/delete tool is refused unless the connection opts in via writeMode: true (or VTEX_WRITE_MODE=true for local dev).

How read vs. write is decided

A single, fail-safe signal: a tool is a read only if it is explicitly annotated readOnlyHint: true. Everything else is treated as a write — including tools with no annotations at all.

This matters: of the ~705 registry tools, only ~303 carry readOnlyHint: true and ~321 have no annotation, and those un-annotated ones are overwhelmingly writes (budget/allocation POSTs, etc.). Treating "no annotation" as write means an un-annotated mutation can never slip through the gate in read-only mode. All genuine reads (including the 5 custom read tools) already carry readOnlyHint: true; the 2 custom writes carry none and are correctly gated.

Why gate at execute time, not by filtering tools/list

@decocms/runtime resolves and caches tool registrations once for the process lifetime (tools.ts if (cached) return cached), while configuration state is delivered per-request (multi-tenant — the same reason tool-adapter.ts already reads credentials only from runtimeContext). Filtering the list by state.writeMode would freeze whatever the first request sent for every subsequent tenant. So the gate reads state per-request at execute time, exactly like credential resolution.

Consequence: write tools still appear in tools/list, but return a clear error if invoked without write mode. The security intent (read-only default, write opt-in) is fully enforced. If deployment is single-tenant, we could additionally hide writes from the list safely — happy to follow up.

Changes

  • server/lib/write-mode.ts (new)resolveWriteMode, isReadOnlyTool, assertWriteModeEnabled.
  • server/types/env.tswriteMode: z.boolean().optional() on StateSchema.
  • server/lib/tool-adapter.ts — every generated tool is gated: non-readOnlyHint tools call assertWriteModeEnabled in execute.
  • server/tools/custom/{reorder-collection,update-product-specifications}.ts — the 2 custom writes gated (they bypass the adapter).
  • app.jsonwriteMode added to configSchema (boolean, default false).
  • README.md, .env.example — docs.
  • server/lib/write-mode.test.ts (new) — 11 tests.

Testing

  • bun test118 pass / 0 fail.
  • tsc --noEmit → no errors in project files (the only 2 errors are internal to @decocms/runtime in node_modules, pre-existing).

🤖 Generated with Claude Code


Summary by cubic

Default VTEX MCP to read-only with opt-in write mode. Previously all tools ran when authorized; now only tools annotated readOnlyHint: true run by default, and any unannotated or destructive tool requires writeMode: true. Write tools still appear in tools/list but return a clear error in read-only mode. Enforcement happens per request at execute time to avoid @decocms/runtime registration caching issues.

  • Added writeMode to state schema and app.json (default false) and VTEX_WRITE_MODE env override for local dev.
  • Added resolveWriteMode, isReadOnlyTool, and assertWriteModeEnabled; integrated gating in tool-adapter.ts and the two custom write tools.
  • Treat only readOnlyHint: true as read; all other tools are writes.
  • Docs updated and tests added.

Rollout/Migration

  • If your workflow needs writes, set writeMode: true in the connection configuration or VTEX_WRITE_MODE=true for local development.
  • No action required for read-only usage.

Written for commit 0d6dde4. Summary will update on new commits.

Review in cubic

Add a `writeMode` config prop to the VTEX MCP state. The MCP is now
read-only by default: read tools (annotated readOnlyHint: true) always
run, while any create/update/delete tool is refused unless the connection
opts in via `writeMode: true` (or VTEX_WRITE_MODE=true for local dev).

A tool is treated as a write unless explicitly annotated readOnlyHint:
true, so un-annotated mutations can never slip through the gate. The gate
is enforced per-request at execute time (not by filtering tools/list),
because @decocms/runtime caches tool registrations for the process
lifetime while configuration state is delivered per-request (multi-tenant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants