- Go 96.9%
- Shell 1.4%
- Dockerfile 1%
- Makefile 0.7%
Adds two stress tests: TestStress_NoLeaksAcross1000Cycles — spawns and reaps 1000 children in sequence, asserts FD count, goroutine count, and zombie status are all stable. TestStress_StopMidLifecycle — 200 cycles that exercise the Stop path (SIGTERM via Close+Signal) rather than relying on natural exit. Bypassed by -short for the unit-test inner loop. Notable findings: * Using the helper-process pattern at this scale was a dead end. Each spawn re-execs the test binary, which inherits the parent's open FDs and runs Go's `testing` package init. Past a few hundred cycles the inner test binaries drag delivery of EOF on their inherited stderr pipe ends, leaving drainStderr goroutines blocked in bufio.ReadString even after Wait returned. Replacing the helper with /bin/true (for quick-exit) and /bin/cat (for echo-loop) sidesteps the recursion and is closer to the production case anyway: the broker spawns forgejo-mcp, not itself. * Defensively close stdout/stderr handles in supervisor's reap goroutine after cmd.Wait returns. cmd.StderrPipe is supposed to be closed by Wait, but under load the kernel doesn't always deliver EOF promptly through Go 1.26's pidfd-based wait path; an explicit Close ensures drainStderr exits and FDs aren't held longer than needed. Tests pass under -race with FD/goroutine deltas in single digits across 1000+200 cycles, and Wait4(-1) confirms no zombie children. Closes forgejo-mcp-broker-31t. Phase 3 complete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .beads | ||
| .claude | ||
| cmd/broker | ||
| docs | ||
| internal | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
forgejo-mcp-broker
OAuth 2.1 authorization server and MCP session broker for forgejo-mcp.
Lets MCP clients such as Claude.ai connect to a Forgejo instance through a single public HTTPS endpoint, with per-user authentication delegated to Forgejo's own OAuth2 provider. The broker handles the OAuth dance, then spawns a dedicated forgejo-mcp --transport stdio subprocess for each authenticated session, scoped to the authenticated user's Forgejo access token.
Status: Planning. No code yet. See docs/design.md for the architecture and docs/plan.md for the phased implementation plan.
How it fits
Claude.ai ──HTTPS──▶ Caddy ──▶ fjmcp-broker ──stdio──▶ forgejo-mcp ──▶ Forgejo API
(this) (one per user (per-user
session) token)
fjmcp-broker(this project): one long-running process. Handles OAuth discovery, dynamic client registration, the authorization-code flow against Forgejo, session lifecycle, and stdio-to-streamable-HTTP bridging.forgejo-mcp(existing project): used as-is. Spawned per-session with the authenticated user'sFORGEJO_ACCESS_TOKENin the environment.- Caddy: terminates TLS for the public hostname and reverse-proxies to the broker.
Why a broker instead of adding OAuth to forgejo-mcp?
Process-level isolation. Each user's Forgejo token lives in exactly one subprocess — the broker never needs to demultiplex tokens inside a single shared client. This keeps forgejo-mcp's sync.Once singleton-client pattern valid and avoids a refactor of every tool handler. Full trade-off in docs/design.md.
Quick map
| File | What |
|---|---|
docs/design.md |
Architecture, components, token flow, deployment, security |
docs/plan.md |
Seven-phase implementation plan with acceptance criteria |
License
MIT © 2026 Ole-Morten Duesund.