# MCP for any client (https://docs.vitrinka.ai/dev/mcp/clients)



The vitrinka server serves MCP over streamable HTTP at `/mcp` (and, for clients that cannot send a header, per workspace at `/w/<workspace>/mcp`). The door speaks OAuth 2.1 (PKCE, dynamic client registration, client-ID metadata documents except under `AIR_GAP=1`, rotating refresh tokens), so every harness connects **with no local process at all** — its own standard remote-URL entry, its own OAuth flow, its own token store. Dispatch stays server-side, no registration file ever carries a credential, and no local stdio forwarder sits between the client and the server any more.

## One grant, one endpoint, a workspace per repo [#levels]

* **One sign-in per origin.** Consent ticks the workspaces the connection may use — by default **All my workspaces, including ones created later**, across every organisation you belong to; Personal stays private unless you tick it. The connection acts as you (member) only there, and only while you are a member of each. The workspace you pick as **default** takes calls that name none. Signing the same app in again starts on the workspaces and default you picked last time. Change the workspaces any time in Settings → Connected apps — no new sign-in.
* **Every entry is the same `/mcp` endpoint.** A **project-level** entry, committed in the repo, adds the header `X-Vitrinka-Workspace: <workspace>` so sessions in that repo land there; Claude Code's **user-level** entry carries no header, and the vitrinka plugin's hook adds the repo's workspace to each call instead. Same URL, same token — no re-authentication when you switch repos.
* **A call that names no workspace** — an unbound directory, a host without the hook — lands in the one workspace the connection reaches that knows its project, else the default workspace. A project several of your workspaces know is refused with each `<workspace>/<project>` spelling instead of guessed.
* **Another workspace, per call.** Spell a project `<workspace>/<project>` (a board `<workspace>/<slug>`, a task, annotation or question id `<workspace>/<id>` on the tools that take only an id — ids are per-workspace — or pass `workspace`, which every tool takes) to act in any other workspace the connection reaches. A call bound to a workspace that lacks its project answers the spelling of the workspace that has it. A workspace the connection does not reach answers `403 workspace_not_connected` with the workspaces it does reach and a `manage_url` that adds it in Settings — never a sign-in prompt.

The server enforces one rule for **user-level connections on the root door**: &#x2A;*through a user-level OAuth connection, writes can only target projects that already exist in the workspace; a new project is created in the web UI or through the workspace door `/w/<workspace>/mcp`.** Neither a repo binding nor a header lets a session silently mint a project into whichever workspace a call landed in. Pinned machine tokens (service and agent tokens, scoped to one workspace) are exempt.

## What `vitrinka setup` renders [#install]

```bash
vitrinka setup                       # Claude Code + every harness detected
vitrinka setup --harness cursor,vscode
vitrinka setup --harness all
```

The repo's committed root `vitrinka.config.json` (`origins`: origin → workspace, beside the `index` policy) is the one manifest; an older `.vitrinka/project.json` is still read for one release and `vitrinka setup` folds it in. `vitrinka setup`, run inside a repo with an **explicit** binding, renders it into each selected harness's project-level config file: it **merges** the `vitrinka` entry into the existing file (other servers, unknown keys, key order and indentation survive), is idempotent, and never rewrites a file it cannot parse as plain JSON — it prints the snippet and says "edit by hand" instead. A repo without a binding gets nothing (bind first: `vitrinka config bind <workspace>`); a machine-default workspace is never baked into a committed file.

The default selection is Claude Code plus every harness whose home directory or CLI is on the machine (`~/.cursor`, `~/.config/opencode` or `opencode`, `code` or `~/.vscode`, `~/.gemini`) or whose config file/directory already exists in the repo. `vitrinka doctor` shows one row per harness file the repo carries — current, stale, or missing against the URL install would write — gray when that harness is not on this machine. Commit the files: they hold an origin and a workspace slug, nothing secret, and every clone is born registered.

Sign in once per harness and origin through the harness's own flow. The token lives in the harness's store; vitrinka's Settings → Connected apps lists each connection, edits the workspaces it reaches and its default, and disconnects it. The committed files spell the URL `${VITRINKA_URL:-https://app.vitrinka.ai}/mcp`, so a self-hosted developer exports `VITRINKA_URL` once and the same file follows; the user-level Claude Code and Codex entries carry the literal origin.

## Claude Code [#claude]

One user-scope entry in `~/.claude.json`, and nothing in the repo:

```bash
claude mcp add --scope user --transport http vitrinka "https://app.vitrinka.ai/mcp?modules=core,pm,me,qa"
# ?modules= names the modules of the plugins you installed — vitrinka setup composes it; the bare /mcp lists every module but rare
```

Sign in once: `/mcp` → vitrinka → authenticate (or `claude mcp login vitrinka`; `--no-browser` on a headless machine). One connection serves every workspace you ticked, and Claude Code keys its token by entry rather than by URL — so a project-scope `.mcp.json` entry, even at the same URL, would cost a separate sign-in in every repo. There is none: the repo's binding lives in `vitrinka.config.json`, and the vitrinka plugin's PreToolUse hook (`vitrinka hook-context bind`) reads it at the session's current directory — worktrees and subagents included — and adds `workspace: "<workspace>"` to every vitrinka call that names none. Any project of that workspace, a board or a bare id then needs no spelling, and two sessions in differently bound repos each land in their own. A call that already names a workspace (`workspace`, `<workspace>/<project>`, `<workspace>/<id>`, a task URL) is left as it is; an unbound repo is left to the server; a `vitrinka.config.json` that cannot be read refuses the call with the file to fix. The rewrite carries no approval of its own: Claude Code's permission rules still judge the call. `vitrinka doctor` names a leftover project-scope entry and `vitrinka setup` removes it (`claude mcp remove --scope project vitrinka`).

## Cursor [#cursor]

`.cursor/mcp.json` (project) — Cursor runs its OAuth flow on first use:

```json
{
  "mcpServers": {
    "vitrinka": { "url": "${VITRINKA_URL:-https://app.vitrinka.ai}/mcp", "headers": { "X-Vitrinka-Workspace": "<workspace>" } }
  }
}
```

The user-level fallback is the same entry with the root `/mcp` in `~/.cursor/mcp.json`.

## OpenCode [#opencode]

`opencode.json` at the repo root:

```json
{
  "mcp": {
    "vitrinka": { "type": "remote", "url": "${VITRINKA_URL:-https://app.vitrinka.ai}/mcp", "enabled": true, "headers": { "X-Vitrinka-Workspace": "<workspace>" } }
  }
}
```

A repo that uses `opencode.jsonc` is not rewritten — install prints this block for you to paste. User level: the same block in `~/.config/opencode/opencode.json` with the root `/mcp`.

## VS Code / Copilot [#vscode]

`.vscode/mcp.json`:

```json
{
  "servers": {
    "vitrinka": { "type": "http", "url": "${VITRINKA_URL:-https://app.vitrinka.ai}/mcp", "headers": { "X-Vitrinka-Workspace": "<workspace>" } }
  }
}
```

VS Code prompts for the OAuth sign-in when the server first starts. User level: **MCP: Open User Configuration** with the root `/mcp`.

## Gemini CLI [#gemini]

`.gemini/settings.json`:

```json
{
  "mcpServers": {
    "vitrinka": { "httpUrl": "${VITRINKA_URL:-https://app.vitrinka.ai}/mcp", "headers": { "X-Vitrinka-Workspace": "<workspace>" } }
  }
}
```

User level: the same entry in `~/.gemini/settings.json` without the header.

## Codex [#codex]

Codex keeps ONE global registry (`~/.codex/config.toml`), so `vitrinka setup` registers the root door there and Codex signs in with its own OAuth flow:

```bash
codex mcp add vitrinka --url https://app.vitrinka.ai/mcp
codex mcp login vitrinka
```

`vitrinka setup` also adds a PreToolUse hook to `~/.codex/hooks.json` (trust it once in `/hooks`): in a bound repo it refuses a vitrinka call that names no workspace and tells the session the `workspace` to add. Codex accepts a rewritten call only together with an approval, so the hook never rewrites. Another workspace is one `<workspace>/<project>` away.

## Any other MCP host [#any-host]

Hosts with OAuth support (Claude Desktop, ChatGPT, …) take the same URL. A host that cannot send headers registers the workspace door `https://app.vitrinka.ai/w/<workspace>/mcp` instead — the same grant, the workspace read from the path. A host that only launches stdio servers is not supported: the forwarder was retired with the organisation-wide grant.

<Callout type="warn">
  Two older registrations are retired: the HTTP shape with an `Authorization` header (it baked a literal credential into config files, including committed ones) and the stdio forwarder `{ "command": "vitrinka", "args": ["mcp"] }`. `vitrinka doctor` flags either and `vitrinka setup` rewrites it. Entries that name `/w/<workspace>/mcp` keep working and are not flagged.
</Callout>

## The catalogue a connection sees [#catalogue]

Tools named `<verb>_<kind>` are not listed, and calling one by its old name answers with the rename: call the generic verb with a kind instead — `list {kind: "work", board}`, `get {kind: "questions", board}` — and read a kind's contract with `docs {topic: "kind:<kind>"}`. A registration can narrow what it lists: append `?modules=core,pm` to its URL; core is always in. Seldom-used doors — `board_working`, `save_template`, `review_judge`, project transfer and merge, favorites — sit in the `rare` module and are listed only when a registration names it; live pairing, for one, needs `board_working`. Naming modules lists only those, so name every one you keep: `…/mcp?modules=core,pm,qa,me,rare`. `vitrinka setup` keeps that opt-in: it leaves such an entry alone, and when it rewrites a registration — another origin, a rebinding, a retired shape — the `?modules=` rides onto the new URL, except from a project-scope `.mcp.json` entry, which is removed with its modules. Any other query, or a value with anything but module names, commas and %-escapes, reads as stale and is rewritten to the plain door. `vitrinka doctor`'s **MCP catalogue** row counts what your Claude Code entry lists. A host setup does not manage may send the modules as an `X-Vitrinka-Modules` header instead; an entry setup manages that carries the header reads as stale and is rewritten without it. Scoping is a listing diet, never an authorization — it does not change what the token may do.

## Listening [#listening]

A board is live only while a session is tuned to it: annotations and released answers reach an agent through `vitrinka work watch`, a leased long-poll that prints one line per work item. What turns that line into a woken session depends on the harness — the listen skill's ladder picks the highest rung available and announces it once, and the board's listening indicator shows the harness and rung it got from the lease.

| Harness                     | How it listens                                                                                                                                                                                                                                                                                  |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude Code                 | Native background `Monitor` on `exec vitrinka work watch --keepalive 0` — the line re-invokes the session, idle costs nothing.                                                                                                                                                                  |
| Codex, Gemini CLI, OpenCode | `vitrinka work listen --harness <name>` hosts the harness over the Agent Client Protocol and injects each work item into the live session as a prompt; `vitrinka work watch --exec <harness>` resumes a thread headlessly for a dedicated listener. Without a host, drain once and end on idle. |
| Cursor, VS Code             | Drain once and end on idle; continuous delivery requires a compatible background host. Never keep a model turn alive by polling an empty queue.                                                                                                                                                 |

```bash
vitrinka work listen --harness codex            # host Codex; work arrives as prompts
vitrinka work listen --harness gemini --board acme-audit
vitrinka work watch --exec codex                # headless: resume the listening thread per item
```

`vitrinka setup` installs the selected harness's ACP adapter and `vitrinka doctor` reports one `listen·<harness>` and `acp·<harness>` row per harness, so a machine that can only listen on the weakest rung says so before a board goes quiet. One live listener holds a scope at a time — arming from another session on the same machine takes it over; a session on another machine is never displaced.

## Reading large replies [#read-budgets]

Default read-only MCP replies above about 2,000 estimated text tokens return
a marked summary with an expansion recipe. Pass an explicit `budget`,
`select` or `jq` to request full or targeted content; the existing 8,000-token
default guard and 30,000-token explicit ceiling still apply. Mutation replies
are not automatically summarized.

The acting `wait_for_work` reply preserves consumed choices, asks and session
notices intact, even above a budget (a `select`/`jq` projection still
applies): those events have already been marked delivered. An idle reply
instead says `action: "end_turn"` and `retry: false`; a third idle poll
within two minutes receives an `idle_poll_loop` correction.

## Skills [#skills]

Skills ship separately from the MCP, as the `vitrinka@kit`, `vitrinka-pm@kit`, `vitrinka-qa@kit` and opt-in `vitrinka-experimental@kit` plugins from the `henderson-tech/vitrinka-kit` marketplace — `vitrinka setup` handles that too, and composes the registration's `?modules=` from the plugins it installed. See [Skills overview](/dev/skills/overview).
