Epic: Migrate the Gitea MCP fleet from local stdio processes to a remote MCP deployment #929

Open
opened 2026-07-26 00:28:12 -05:00 by jcwalker3 · 0 comments
Owner

Problem

The Gitea MCP fleet runs as five local operating-system processes on the operator's Mac. Each one is mcp_server.py launched by the LLM client, pinned to a single role by the GITEA_MCP_PROFILE environment variable, reading its token from the operator's macOS keychain, and speaking JSON-RPC over stdio pipes owned by that client.

That model is load-bearing in the code, not incidental. The transport is bound literally, once, at process start:

# gitea_mcp_server.py:23750
mcp_daemon_guard.bind_native_mcp_transport(transport="stdio")

and the safety architecture is built on top of that bind. Mutations fail closed unless the process can prove it was launched by a client with real stdio pipes and GITEA_CLIENT_MANAGED provenance; the master-parity gate compares the commit the process started at against the working tree on the same disk; author tools operate directly on local branches/ worktrees; and every lock, lease, and session record lives in local files keyed to a local PID.

The consequences of that coupling are the reason to move:

  • Capacity is one fleet per workstation. A second operator, a CI runner, or a cloud agent cannot attach at all.
  • Five processes must be respawned, and the whole fleet re-verified, on every client reconnect.
  • Role separation is enforced by which process a call reaches, so it cannot survive a shared endpoint without a per-request principal.
  • Runtime freshness is defined as "this process's startup commit matches this disk", which has no meaning for a host that does not carry the operator's checkout.
  • Credentials are bound to one human's keychain, so the trust boundary cannot be audited or rotated centrally.

Children

Ten children, dependency-ordered. A child is implementation-ready only when every blocker is closed.

# Issue Child Depends on Status
1 #930 Inventory every stdio- and localhost-coupled assumption in the MCP server ready
2 #931 Introduce a transport-neutral bind seam accepting a remote transport #930 blocked
3 #932 Replace per-process profile binding with per-request principal resolution #930, #931 blocked
4 #933 Replace macOS-keychain token resolution with a server-side credential provider #932 blocked
5 #934 Define remote-session provenance to replace the stdio client-managed guard #931, #932 blocked
6 #935 Redefine the master-parity mutation gate for a remote deployment #931 blocked
7 #936 Split local-filesystem tools from remotable tools and fail closed on the boundary #930 blocked
8 #937 Move session, lock, and lease state to a store safe for concurrent remote sessions #932, #936 blocked
9 #938 Stand up the authenticated remote MCP endpoint #933, #934, #935, #937 blocked
10 #939 Dual-run cutover, parity validation, and rollback runbook #938 blocked

Dependency order, as a critical path:

#930 ─┬─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐
      │         │         ├─> #934 ─────────────┤
      │         └────────────> #935 ────────────┤
      │                   └─> #937 ─────────────┤──> #938 ──> #939
      └─> #936 ────────────────> #937 ──────────┘

The single implementation-ready child is #930. Everything else is blocked until its blockers close.

Epic completion gate

Do not decommission the local stdio fleet until all of the following hold:

  • #930 through #939 are closed.
  • A remote endpoint serves all five roles with per-request principal resolution, and no role's permissions can be obtained by a caller holding another role's credential.
  • No mutation path reads the operator's macOS keychain.
  • Every tool that requires the operator's local filesystem is explicitly classified, and calling one against the remote endpoint fails closed with a named blocker rather than acting on the wrong disk.
  • The runtime-freshness gate returns a defined, testable verdict on the remote deployment.
  • Two different LLM clients complete a supervised end-to-end canary against the remote endpoint for the author, reviewer, and merger roles.
  • The rollback runbook has been executed once, on purpose, and the local fleet was restored from it.

Non-goals

  • Changing the role taxonomy or the permission sets attached to each role.
  • Changing Gitea-side branch protection or the review and merge state machine.
  • Rewriting the author transactional-safety work tracked by issue #887.
  • Multi-tenant hosting for organizations beyond this one.

Duplicate verdict

No existing issue covers this scope. Verified against a full scan of every issue in this repository, numbers 1 through 927. Issue #887 lists replacing the MCP transport and role-namespace model as an explicit non-goal. Issue #631 is a web console for the control plane, a different surface. Issue #689 is deterministic namespace attachment within the current local model. Issue #808 concerns worker discovery, not transport.

## Problem The Gitea MCP fleet runs as five local operating-system processes on the operator's Mac. Each one is `mcp_server.py` launched by the LLM client, pinned to a single role by the `GITEA_MCP_PROFILE` environment variable, reading its token from the operator's macOS keychain, and speaking JSON-RPC over stdio pipes owned by that client. That model is load-bearing in the code, not incidental. The transport is bound literally, once, at process start: ``` # gitea_mcp_server.py:23750 mcp_daemon_guard.bind_native_mcp_transport(transport="stdio") ``` and the safety architecture is built on top of that bind. Mutations fail closed unless the process can prove it was launched by a client with real stdio pipes and `GITEA_CLIENT_MANAGED` provenance; the master-parity gate compares the commit the process started at against the working tree on the same disk; author tools operate directly on local `branches/` worktrees; and every lock, lease, and session record lives in local files keyed to a local PID. The consequences of that coupling are the reason to move: - Capacity is one fleet per workstation. A second operator, a CI runner, or a cloud agent cannot attach at all. - Five processes must be respawned, and the whole fleet re-verified, on every client reconnect. - Role separation is enforced by which process a call reaches, so it cannot survive a shared endpoint without a per-request principal. - Runtime freshness is defined as "this process's startup commit matches this disk", which has no meaning for a host that does not carry the operator's checkout. - Credentials are bound to one human's keychain, so the trust boundary cannot be audited or rotated centrally. ## Children Ten children, dependency-ordered. A child is implementation-ready only when every blocker is closed. | # | Issue | Child | Depends on | Status | | - | ----- | ----- | ---------- | ------ | | 1 | #930 | Inventory every stdio- and localhost-coupled assumption in the MCP server | — | ready | | 2 | #931 | Introduce a transport-neutral bind seam accepting a remote transport | #930 | blocked | | 3 | #932 | Replace per-process profile binding with per-request principal resolution | #930, #931 | blocked | | 4 | #933 | Replace macOS-keychain token resolution with a server-side credential provider | #932 | blocked | | 5 | #934 | Define remote-session provenance to replace the stdio client-managed guard | #931, #932 | blocked | | 6 | #935 | Redefine the master-parity mutation gate for a remote deployment | #931 | blocked | | 7 | #936 | Split local-filesystem tools from remotable tools and fail closed on the boundary | #930 | blocked | | 8 | #937 | Move session, lock, and lease state to a store safe for concurrent remote sessions | #932, #936 | blocked | | 9 | #938 | Stand up the authenticated remote MCP endpoint | #933, #934, #935, #937 | blocked | | 10 | #939 | Dual-run cutover, parity validation, and rollback runbook | #938 | blocked | Dependency order, as a critical path: ``` #930 ─┬─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐ │ │ ├─> #934 ─────────────┤ │ └────────────> #935 ────────────┤ │ └─> #937 ─────────────┤──> #938 ──> #939 └─> #936 ────────────────> #937 ──────────┘ ``` The single implementation-ready child is #930. Everything else is blocked until its blockers close. ## Epic completion gate Do not decommission the local stdio fleet until all of the following hold: - #930 through #939 are closed. - A remote endpoint serves all five roles with per-request principal resolution, and no role's permissions can be obtained by a caller holding another role's credential. - No mutation path reads the operator's macOS keychain. - Every tool that requires the operator's local filesystem is explicitly classified, and calling one against the remote endpoint fails closed with a named blocker rather than acting on the wrong disk. - The runtime-freshness gate returns a defined, testable verdict on the remote deployment. - Two different LLM clients complete a supervised end-to-end canary against the remote endpoint for the author, reviewer, and merger roles. - The rollback runbook has been executed once, on purpose, and the local fleet was restored from it. ## Non-goals - Changing the role taxonomy or the permission sets attached to each role. - Changing Gitea-side branch protection or the review and merge state machine. - Rewriting the author transactional-safety work tracked by issue #887. - Multi-tenant hosting for organizations beyond this one. ## Duplicate verdict No existing issue covers this scope. Verified against a full scan of every issue in this repository, numbers 1 through 927. Issue #887 lists replacing the MCP transport and role-namespace model as an explicit non-goal. Issue #631 is a web console for the control plane, a different surface. Issue #689 is deterministic namespace attachment within the current local model. Issue #808 concerns worker discovery, not transport.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#929