# Server configuration

The Modyo MCP server is configured entirely through environment variables,
read once at startup. To change configuration, restart the server.

## `MODYO_MCP_MODULES` — module filtering

Restrict which modules the server exposes. This shrinks the `tools/list`
footprint and the `instructions` handshake to only the modules you need for a
session.

- **Default (unset or empty):** all modules active — identical to previous
  behavior. Existing integrations need no change.
- **Syntax:** comma-separated list, e.g. `channels,content`. Surrounding
  whitespace is tolerated; duplicates are ignored; the order you list them in
  does not matter.
- **Case-sensitive:** only lowercase names are valid.
- **Valid values:** `core`, `content`, `channels`, `customers`, `widgets`.

```bash
# All modules (default)
node build/index.js

# Only channels + content
MODYO_MCP_MODULES=channels,content node build/index.js

# A single module
MODYO_MCP_MODULES=customers node build/index.js
```

### What gets filtered

Filtering applies consistently to the four catalog surfaces:

| Surface | Behavior |
|---|---|
| **Tools** | Only the active modules' tools are registered. |
| **Resources** | Transversal resources (`platforms://list`, `modyo://docs/tools/*`, `modyo://docs/widgets/*`, `modyo://context/agent-rules`) are **always** active. Module-bound resources (e.g. the Liquid reference → channels, the widgets catalog → widgets) require their module. |
| **Prompts** | Module-bound prompts require their module. A **workflow** prompt (e.g. `modyo-create-blog`, which spans channels + content) is exposed only when **all** its modules are active. |
| **`instructions`** | Composed from per-module fragments; only active modules' sections appear. No orphan headings or blank-line gaps. |

### Fail-loud validation

An invalid value stops startup with an explicit error (rather than silently
serving a reduced catalog — a typo in `mcp.json` would otherwise be hard to
notice):

```
Invalid module(s) in MODYO_MCP_MODULES: 'channelz'.
Valid values: core, content, channels, customers, widgets.
```

When filtering is active, a summary is logged at startup:

```
Module filtering active. Modules enabled: channels, content. Filtered out: core, customers, widgets.
Tools: 49 (filtered from 80). Resources: 6 (filtered from 11). Prompts: 8 (filtered from 9).
```

### Not covered

Module filtering resolves **focus and catalog size**, not API permissions
(that would require a Modyo API endpoint that does not yet exist). A filtered
module's repositories still exist in the process; only their catalog entries
are withheld.

## `MODYO_READ_ONLY` — read-only mode

Guarantees the server executes **no mutation**, even if the token has write
permissions. Intended for auditing / exploring / suggesting improvements on
client sites in production.

- **Activation:** `MODYO_READ_ONLY=true`. Any other value (or unset) = off
  (current behavior).
- **Semantics:** blocks **all** mutation (both write and destroy). Only reads
  execute.
- **Composable with modules:** applied after `MODYO_MCP_MODULES`. The combo
  `MODYO_MCP_MODULES=channels` + `MODYO_READ_ONLY=true` is the audit use case
  (issue #58): only Channels read tools.

```bash
MODYO_MCP_MODULES=channels MODYO_READ_ONLY=true node build/index.js
```

### How it enforces (allowlist, fail-closed)

Two layers:

1. **Skip registration** — single-purpose write/destroy tools (e.g.
   `customers-realms-delete`, the `*-upsert` tools) are not registered at
   all. Pure read tools (those declaring `readOnlyHint`) are always kept.
   Generate-local tools without `readOnlyHint` (they create files on the
   operator's machine without mutating the site — e.g. `widgets-scaffold`)
   also classify as mutation and are skipped: audit mode does not scaffold
   projects.
2. **Per-action gate** — multi-action `Manage` tools stay registered (they
   also serve `list`/`get`), but at dispatch only a small **allowlist** of
   read actions is permitted:

   ```
   list · get · search · find
   ```

   Everything else (`create`, `update`, `upsert`, `clone`, `publish`,
   `unpublish`, `archive`, `restore`, `delete`, `bulk_delete`, …) is denied by
   default — a new or unknown action can never slip through. A blocked call
   returns a structured refusal without executing:

   ```json
   { "readOnly": true, "rejected": "delete", "reason": "Server running in read-only mode (MODYO_READ_ONLY). Only read actions are permitted." }
   ```

**The `manage` action** is dual (get when no fields, update when fields are
present). It is allowed only when the payload carries **no mutating field**
(just identifier/scope/pagination). Modules whose `manage` does not follow
this pattern — notably `core/groups` (which uses `groupName`/`groupId` and
documents `manage` as create/update) — have `manage` treated as mutating and
blocked; their `list` action still works.

**Tools declared read-only run fully.** The per-action allowlist governs the
potentially-mutating multi-action tools. A tool that declares
`readOnlyHint: true` (the MCP-standard "does not modify its environment"
signal — e.g. `channels-download`, `channels-preview`) is trusted and runs
all its actions, even domain verbs outside the allowlist (`start`, `status`,
`page`, …), because those tools do not mutate the site. Read-only mode is
therefore only as strong as the `readOnlyHint`/`destructiveHint` annotations;
a regression test enforces that no tool declares both `readOnlyHint: true` and
`destructiveHint: true`, so a contradictory annotation cannot silently open a
hole. (The structural refactor in #106 removes this trust dependency.)

Startup log when active:

```
Read-only mode active (MODYO_READ_ONLY). Read tools exposed: 20. Multi-action tools gated: 25. Write/destroy tools skipped: 35.
```

### Interim vs structural

This is a **runtime** gate — an interim delivery. It does not give the
compiler-enforced guarantee of the structural `read`/`write`/`destroy` split
(issue #106, deferred to 0.5.0); when that lands, the read-only guarantee
moves into the tool structure and this runtime gate can be retired.
