# Static Dual-Namespace Gitea MCP Deployment ## Purpose This document (tracked as issue #143) records the deployment model accepted in issue #139: run the Gitea MCP server as **two static, per-role namespaces** — one authoring, one reviewing — instead of switching profiles inside a running server or routing through a dispatcher. It explains what to configure, why this model was chosen, and what to expect from MCP clients. This is the deployment companion to [`gitea-execution-profiles.md`](gitea-execution-profiles.md) (the profile *model*) and [`llm-workflow-runbooks.md`](llm-workflow-runbooks.md) (the workflows run on top of it). ## The model Run two independent MCP server instances of the same `gitea-mcp` code, each launched with exactly one static execution profile: | Namespace (MCP server name) | Profile (role) | Typical use | |-----------------------------|----------------|-------------| | `gitea-author` | an author profile | implement issues, push branches, open PRs, comment | | `gitea-reviewer` | a reviewer profile | review, approve/request changes, merge | Properties: - **One process, one credential.** Each namespace authenticates as exactly one Gitea identity for its entire lifetime. A session connected to `gitea-author` can never approve or merge; a session connected to `gitea-reviewer` cannot push branches or commit unless explicitly configured. - **`runtime_switching_supported: false`.** The running server never changes identity. Choosing a role means choosing which namespace to connect to, not asking the server to become someone else. - **Roles are profiles, not LLMs.** Per the profile model, the LLM is not the role — the profile is. The same LLM session may author under one namespace and (in a *separate* session) review under the other. ## Rejected alternatives — and why Both alternatives below were considered in the #139 discussion and are **rejected for now**; dynamic in-process profile switching is **not enabled in this deployment model**. (The runtime *can* support it behind an explicit `allow_runtime_switching: true` config opt-in — see [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — but this model deliberately leaves it off, so namespaces report `runtime_switching_supported: false`.) - **Dynamic profile switching** (one server, `gitea_activate_profile`-style role changes at runtime): rejected because a single process would hold, or be able to obtain, both credentials; "which identity am I?" becomes mutable state that injected instructions could target; and audit attribution blurs when one process acts as multiple identities. - **Dispatcher / router front door** (one entry point that forwards each call to a role-appropriate backend): rejected because it concentrates every credential behind one surface and re-creates the same escalation problem with extra moving parts. Why the static dual-namespace model wins: - **Clearer audit.** Every audit record from a namespace maps to one identity and one `audit_label`; there is no in-process identity history to reconstruct. - **Less credential concentration.** No process ever holds more than one token. Compromise or prompt-injection of one session bounds the blast radius to that role's allowed operations. - **Simpler two-party review boundary.** Author and reviewer are different authenticated identities in different processes; self-review/self-merge checks stay structural, not behavioral. Note that namespaces alone do not provide two-party review — one agent driving both namespaces in one session still defeats it. Keep authoring and reviewing in separate sessions. - **Safer fail-closed behavior.** Each server validates its single profile at startup and on every gated call; anything unknown, ambiguous, or unresolved refuses. There is no "switch succeeded but half-applied" state to reason about. ## Client setup Each namespace is the same server binary launched with its own environment. Configuration is by *reference only*: environment variables name a config file and a profile entry; tokens stay in the operator's keychain/secret store and never appear in client config, tool output, or this document. Conceptual client registration (names and variables only — adapt the launch syntax to the client): ```jsonc { "mcpServers": { "gitea-author": { "command": "/venv/bin/python3", "args": ["/mcp_server.py"], "env": { "GITEA_MCP_CONFIG": "", "GITEA_MCP_PROFILE": "" } }, "gitea-reviewer": { "command": "/venv/bin/python3", "args": ["/mcp_server.py"], "env": { "GITEA_MCP_CONFIG": "", "GITEA_MCP_PROFILE": "" } } } } ``` - `GITEA_MCP_CONFIG` — path to the operator-owned profiles config (see [`gitea-execution-profiles.md`](gitea-execution-profiles.md)). The file is operator-owned; LLM sessions must never rewrite it. - `GITEA_MCP_PROFILE` — the profile entry this namespace runs as. Exactly one per namespace; never both. - Verify after connecting: call `gitea_whoami` / `gitea_get_runtime_context` and confirm the authenticated identity and allowed operations match the namespace's role before doing any work. ### "Auth unsupported" in some clients is normal Some MCP clients display an "Auth unsupported" (or similar) status for custom/local stdio servers. That message refers to the client↔server MCP authentication handshake, which local servers do not use — it does **not** mean Gitea authentication failed. Gitea credentials are resolved by the server itself from the configured profile. Trust `gitea_whoami`, not the client's connection badge. ## Reconnect / reload after changes The server reads its code and profile config **once, at process start**. A long-running namespace does not see later changes, so after any of: - editing the profiles config (e.g. granting/removing an operation), - merging server code that changes operation gating or tool surfaces, - rotating the credential a profile references, the operator must **reload** the affected namespace — restart the server or use the client's MCP reconnect action (e.g. `/mcp` in Claude Code) — before the change takes effect. Symptoms of a stale namespace include gated calls failing closed with operation-normalization errors even though the live config is correct. Fail-closed is the intended behavior here: a stale server refuses rather than guesses. Reconnect and re-verify with `gitea_whoami`. ## Related documents - [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model, reference profiles (`gitea-author`, `gitea-reviewer`), operation naming, and safety rules. - [`llm-workflow-runbooks.md`](llm-workflow-runbooks.md) — the author and reviewer workflows run on top of these namespaces. - [`safety-model.md`](safety-model.md) — fail-closed and gating principles. - Issue #139 — the discussion and decision this document records.