vitrinka.docs

MCP for any client

Register the vitrinka MCP server with Claude Code, Cursor, OpenCode, VS Code, Gemini CLI, Codex or any MCP host.

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

  • 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: 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

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

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

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/mcp.json (project) — Cursor runs its OAuth flow on first use:

{
  "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.json at the repo root:

{
  "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/mcp.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/settings.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 keeps ONE global registry (~/.codex/config.toml), so vitrinka setup registers the root door there and Codex signs in with its own OAuth flow:

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

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.

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.

The catalogue a connection sees

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

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.

HarnessHow it listens
Claude CodeNative background Monitor on exec vitrinka work watch --keepalive 0 — the line re-invokes the session, idle costs nothing.
Codex, Gemini CLI, OpenCodevitrinka 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 CodeDrain once and end on idle; continuous delivery requires a compatible background host. Never keep a model turn alive by polling an empty queue.
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

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 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.

On this page