[EPIC] Install forgejo-mcp on the Guest as a LAN-reachable Service #17
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
wontfix
needs-info
needs-triage
ready-for-agent
ready-for-human
review/merge-ready
review/needs-fix
review/needs-human
review/needs-review
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
olympus/infra-forge#17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
An Agent reaches the Forgejo instance at
forgejo.thepit.spaceonly by shelling out tocurl+jqagainst the Gitea-style REST API, followingdocs/agents/issue-tracker.md. That convention works, but it is a document, not a mechanism: every Agent must read it, compose raw HTTP by hand, and parse JSON with no schema. Nothing type-checks the calls, and nothing bounds what an Agent may do to the tracker beyond the token it happens to hold — today the same broad Admin-scoped token in~/.config/forgejo/.env, with no split between "read an issue" and "rewrite the repository".Solution
Run the upstream
forgejo-mcpMCP server as a Service on theforgejoGuest, LAN-reachable athttp://10.12.0.141:8089/mcp, provisioned by a new Ansible role and pinned like the other two Services (Forgejo, the Actions runner).The endpoint is credential-less. In its default
passthroughauth mode every request must carry its own Forgejo token, and a request without one is refused401, so the Service holds no credential that could be stolen from it. An Agent on the LAN points its MCP client at the endpoint and presents a token scoped to its job — the token, not the server, is the capability boundary.User Stories
pit/infra-forge, so that I can find the ticket I am working on.write:issue, so that an Agent can triage issues but cannot push code, merge a PR, or change repository settings.repositorywrite category, so that the blast radius of a leaked Agent token stops at issues.10.12.0.0/24, so that the homelab's agent interface is not addressable from the internet.401, so that I can use a probe as a liveness check.Implementation Decisions
Shape. A new Ansible role, separate from the
forgejorole, with its own play insite.yml. Theforgejorole is a six-section linear flow whose handler definition order is a load-bearing contract (the runner restart must land after the Forgejo restart); a second Service as its own role leaves that contract untouched and gives the new pin a convention-4 home.Packaging and pinning. The release tarball
forgejo-mcp_3.2.0_linux_amd64.tar.gzfrom the upstream forgegit.b4mad.industries, pinned in the role defaults alongside its version and port (convention 4: one pin per Service). Upstream publishes a single multi-artifactforgejo-mcp_3.2.0_checksums.txtand no per-file.sha256, so "checksum-first" here means pinning the literal sha256 of the archive (bf8f744d53dd06c0e7830ee13a0507464b3ab301fcf01de4744db03d770039df, verified by download-and-hash) rather than pointing at a remote checksum URL — a stronger pin than the service binary's pattern, not a weaker one. The signed OCI image was rejected: it islinux/amd64only and would introduce a persistent-container pattern into a repo whose only Docker use today is ephemeral Actions job containers.Transport. Streamable HTTP (
/mcp), not SSE (upstream's own "legacy" label) and not stdio. The Agent client path ismcp.client.streamable_http.Binding and identity of the endpoint. The Service binds the Guest's interface (
0.0.0.0) and declares a host allowlist of10.12.0.141. A bare allowlist entry matches any port, so the single entry suffices. This is fail-closed: upstream refuses to start when bound to a non-loopback address with no allowlist, and answers403to a request whoseHostis not declared. No--allowed-originsis set: a non-browser MCP client sends noOrigin, and only a present but unlistedOriginis rejected.Authentication.
passthrough(the default), with--allow-operator-token-fallbackleft off. No token is configured on the Service at all; the server boots without one and uses its own token, if any, only for a startup reachability check. Turning the fallback on would make any LAN client that reaches the port act as the identity behind the configured token — explicitly warned against upstream and rejected here.Upstream connection. The Service reaches Forgejo at loopback, not through the Edge; it runs on the same Guest as Forgejo, so no DNS hairpin or proxy round-trip is involved.
ROOT_URLandDOMAINare already set inapp.ini, so Forgejo's API responses carry the public URL and the MCP server does not need to rewrite them.Exposure. LAN-reachable by address only: no DNS record, no NPM proxy host, no certificate, no router rule. Not WAN-published.
Service lifecycle. A systemd unit under a dedicated service user. There is no health endpoint and no signal handler upstream: readiness is a TCP probe of the MCP path expecting
401, optionally corroborated by thelistening on a network-reachable addressline on stderr;SIGTERMgets the default disposition, which is what systemd's stop wants.Token scope (the identity decision). Each Agent presents its own Forgejo token. Forgejo 15 splits the
issuecategory out ofrepository, so the grant is tight: triage =write:issue(write implies read); browsing PRs and code = addread:repository;write:repository— which unlocks contents, Actions, PR merge and push — is refused. Read-only start is a one-scope downgrade toread:issue(±read:repository). The MCP tool surface includes mutations (merge_pull_request, writes and deletes); the token scopes them out, and that is the only boundary — the server enforces none of it.Secrets. No new Vault entry: the Service is credential-less in
passthroughmode and holds no Forgejo token. Nothing new is committed in ciphertext.Client half (documented, not provisioned). Installing the
mcpPython package and itsmcp.client.streamable_httpextra, adding themcp_servers:entry, and restarting the Agent (no hot-reload) is a runbook section, because it edits a workstation config, not this repo's IaC.Testing Decisions
A good test exercises external behaviour only — a request and its observable answer, or a run and its reported change count — never the role's internals. Three seams, all existing points of the stack, no new seam:
0 changed. This is the repo's established seam.:8089/mcpfrom another LAN host answers401(the readiness signal), and anmcpclientinitialize+tools/listround-trip succeeds when the request carries a valid token.write:issuetoken and is refused403with a token lacking theissuecategory; awrite:issuetoken is refused403on a PR-merge or file-write call. This asserts the one boundary that matters.Prior art: the role's existing "wait for the instance to see the runner online" retry probe, and the README's verify steps (curl status assertions, "second run of the playbook = 0 changed"). The whole role is rehearsed on the PRE Guest before Prod, per the deploy runbook.
Out of Scope
mcp.thepit.space): there is no DNS mechanism in this repo. LAN-reachable by IP only.curl+jqissue-tracker convention; it remains the fallback.Further Notes
GLOSSARY.md: Agent, Operator, Service, and the LAN-reachable / WAN-published split.git.b4mad.industries/agentic-forges/forgejo-mcp. Latest release isv3.2.0(2026-09-16); the old mirrors lag (≈v2.30.2). Issue numbers are preserved across the move.10.12.0.0/24; upstream'sSECURITY.mdsays to treat the port as sensitive. Consistent with LAN-reachability today (nothing on the guest is firewalled).Sub-tasks
Split into four tickets, published in dependency order. Each blocking edge is
set natively on the tracker, so the dependency links — not prose — carry it.
pit referenced this issue2026-10-06 12:46:08 +00:00
Install forgejo-mcp on the Guest as a LAN-reachable Serviceto [EPIC] Install forgejo-mcp on the Guest as a LAN-reachable ServiceClosing: all four sub-tasks are merged to
mainand closed.docs/adr/0003-forgejo-mcp-service-shape-and-endpoint.md)forgejo_mcprole, its play insite.yml, and the pin in the role defaultsdocs/adr/0005-agent-token-scope.md) and the capability seam verified live on PRE (#56)Body corrected: the Further Notes ADR numbers were stale (0002/0003) and now read 0003/0005, which is what landed.