# 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 |
| `gitea-merger` | a merger profile | merge PRs after approval and verification |
| `gitea-reconciler` | a reconciler profile | close already-landed open PRs after ancestry proof (#304 profile; #310 close tool) |

Review and merge are separate workflow roles. A reviewer approval is not merge authorization.

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": "<path-to>/venv/bin/python3",
      "args": ["<path-to>/mcp_server.py"],
      "env": {
        "GITEA_MCP_CONFIG": "<path-to-profiles.json>",
        "GITEA_MCP_PROFILE": "<author-profile-name>"
      }
    },
    "gitea-reviewer": {
      "command": "<path-to>/venv/bin/python3",
      "args": ["<path-to>/mcp_server.py"],
      "env": {
        "GITEA_MCP_CONFIG": "<path-to-profiles.json>",
        "GITEA_MCP_PROFILE": "<reviewer-profile-name>"
      }
    },
    "gitea-merger": {
      "command": "<path-to>/venv/bin/python3",
      "args": ["<path-to>/mcp_server.py"],
      "env": {
        "GITEA_MCP_CONFIG": "<path-to-profiles.json>",
        "GITEA_MCP_PROFILE": "<merger-profile-name>"
      }
    }
  }
}
```

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