# Configuration format

English | [中文](format.zh.md)

[← README](../../README.md) ｜ Related: [configuration sources and layers](layers.md) · [`${VAR}` expansion](env-expansion.md) · [CLI `dsh-mcp`](cli.md)

Two dialects are read by the plugin: the **native YAML managed block**
(`<projectRoot>/.dsh/mcp.yml`, `~/.dsh/mcp.yml`, `~/.dsh/profiles/<name>/mcp.yml`)
and the **JSON dialect** (`<projectRoot>/.dsh/mcp.json`, `~/.dsh/mcp.json`,
`~/.dsh/profiles/<name>/mcp.json`, plus the read-only legacy Claude Code project
file `<projectRoot>/.mcp.json`). Which file belongs to which layer is described
in [configuration sources and layers](layers.md). The
[settings page](settings-ui.md) writes only the managed yml files, through the
same managed block and lock as the CLI.

## Native YAML: `<projectRoot>/.dsh/mcp.yml`

Uses the same managed-block format as the profile `cordis.patch.yml` (a YAML
`insert` list between begin/end markers), with one MCP server per line:

```yaml
# >>> dsh-project-mcp-manager:mcp:begin
- insert:
    - id: panel-mcp-gitlab
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: gitlab
        transport: stdio
        command: npx
        args: ['-y', '@modelcontextprotocol/server-gitlab']
        cwd: .            # resolved relative to the project root
        toolCallTimeoutMs: 60000
        failOnStartupError: false
        reconnect:
          enabled: true
          initialDelayMs: 500
          maxDelayMs: 30000
          maxAttempts: 10
# <<< dsh-project-mcp-manager:mcp:end
```

`transport` supports `stdio` (command/args/env/cwd) and `streamable-http`
(url/headers). Add `disabled: true` to a line to deactivate it. Content outside
the markers is preserved byte-for-byte.

Per-entry tool visibility (stripped before `ctx.plugin`; deny wins):

```yaml
        tools:
          allow: ["read_*"]
          deny: ["read_secret", "mcp__gitlab__delete_*"]
```

Patterns are globs (`*`, `**`, `?`, `[…]`) and match either the bare tool name
or a full `mcp__<effectiveName>__<tool>` id. A full-id pattern is compared
against the **effective** name the loader actually registered (`mcp__<effective>__…`),
not the `serverName` written in the file. Project rows that were renamed
(`p<hash>_…`) therefore do **not** match `mcp__<originalName>__foo`; use the
bare tool name, or look up the effective name in `dsh-mcp get` / a snapshot.
Illegal character classes (for example `[z-a]`) warn once per entry and never
match, instead of failing silently. JSON also accepts Gemini's
`includeTools` → `allow` and `excludeTools` → `deny`; if both the DSH `tools`
object and those keys are present, `tools.allow` / `tools.deny` win.

**Divergences from the native cordis dialect**: the `!!js` tag (a js-yaml
expression evaluated by the profile loader, e.g. the official README's
`env: { TOKEN: !!js process.env.GITHUB_TOKEN }`) is **not supported** in
project files — an unresolved tag inside the managed block makes the whole
file fail with an explicit error (logged and written to `$DSH_HOME/mcp-diag/<hash>.json`)
instead of silently mounting the expression text as a literal string. Values
in `env`/`headers` are otherwise literal, except for `${VAR}` references
which are interpolated at mount time (see [`${VAR}` expansion](env-expansion.md));
`disabled` must be `true`/`false`. Optional `maxInstructionBytes` (integer ≥ 1)
is the official cap on attributed server instructions, default 32768 when the
key is omitted. An oversized instruction rejects that connection inside
`dsh-mcp-client` (the row still activates when `failOnStartupError` is false,
then follows the official reconnect loop). `dsh-mcp` has no flag for this
field; write it in the file. The project file is otherwise a superset
grammar: `env`/`headers` accept `KEY: null` to delete a key (stripped at
mount), which the official mcp-client schema rejects — such lines would fail
if moved back to `cordis.patch.yml`.

## JSON dialect

Follows the ecosystem (Cursor / Claude Code's `mcpServers` shape):

```json
{
  "mcpServers": {
    "gitlab": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-gitlab"],
                "env": { "GITLAB_TOKEN": "${GITLAB_TOKEN}" }, "cwd": "." },
    "sentry": { "url": "https://mcp.sentry.dev/mcp",
                "headers": { "Authorization": "Bearer ${SENTRY_TOKEN}" } }
  }
}
```

- `command`/`args`/`env`/`cwd` are stdio; `url`/`headers` are streamable-http;
  an optional `type` (`stdio`|`http`|`streamable-http`). An entry that gives
  both `command` and `url`/`httpUrl` without `type`/`transport` fails per
  entry instead of silently loading as stdio — spell the transport. `type: "sse"` is the
  **MCP SSE endpoint transport** (protocol version 2024-11-05) and is rejected
  per entry with an actionable diagnostic: the mount backend
  (`dsh-mcp-client`) only speaks `stdio` and streamable-http — if the server
  already speaks Streamable HTTP, change `type` to `"http"`; or drop `type`
  and keep `url` (this plugin infers streamable-http). There is no implicit
  fallback. The DSH passthrough keys
  `toolCallTimeoutMs`/`failOnStartupError`/`maxInstructionBytes`/`reconnect`, `enabled: false`
  (silently skipped, claims no name) and `disabled: true` (claims its name but
  is not mounted).
- Unknown keys are tolerated and ignored; names must match
  `[A-Za-z0-9_-]{1,32}`; bad entries error per entry and do not affect the
  rest.
- **JSON files are owned exclusively by the `dsh-mcp` CLI**: writes preserve
  other top-level keys and key order, but JSON has no comments, so formatting
  and comments are not preserved. The host side (the loader and the settings
  page) is **read-only** for JSON and never writes these files; editing a
  JSON-sourced row in the settings page writes the managed yml of the same
  scope instead.
- `${VAR}` has the same semantics as every other source (expanded at mount
  time, see [`${VAR}` expansion](env-expansion.md)).

### `httpUrl`, `transport`, and `url` inference

- Gemini CLI writes `httpUrl` for Streamable HTTP. This plugin accepts it as
  an explicit streamable-http URL. If both `url` and `httpUrl` are present
  with **different** values, the entry errors (the message does not echo the
  URLs). Identical values are tolerated.
- Native YAML uses `transport: stdio | streamable-http`. The JSON dialect
  accepts the same `transport` key. When both `transport` and `type` are set
  and they map to different official transports, the entry errors;
  `transport` is preferred when they agree (native dialect wins).
- **`url` without `type`/`transport` is Streamable HTTP here**, which is the
  **opposite** of Gemini CLI (`url` = MCP SSE endpoint transport, `httpUrl` =
  Streamable HTTP). To copy a Gemini `mcpServers` block into `.dsh/mcp.json`:
  keep `httpUrl` as-is (loaded as streamable-http); convert Gemini `url`
  (SSE) by either pointing the server at a Streamable HTTP endpoint and
  renaming the field to `httpUrl` / `type: "http"`, or leaving it as
  `type: "sse"` and reading the actionable diagnostic.
- This plugin does **not** treat a bare `url` as MCP SSE. That inference is
  documented so a file that works in Gemini is not silently given the
  opposite meaning.
