feat(routing): project_create MCP tool — gitea-first new-project pipeline (#10)
All checks were successful
CI / Lint / Test / Vet (push) Successful in 12s
CI / Mirror to GitHub (push) Successful in 4s

Adds the project_create tool to the routing pod that automates the
"new project" bootstrap end-to-end from claude.ai. Gitea-first
architecture: GitHub receives the repo only via push-mirror, never
via a direct GitHub API call from this server.

Four sequential calls to the gitea-mcp server (configured via
GITEA_MCP_URL):

  1. create_project_from_template — Gitea repo from
     template-go-{agent,web} per the 'stack' arg
  2. repo_mirror_push (action=add) — push-mirror to
     github.com/<GITHUB_OWNER>/<name>.git, interval 8h, sync_on_commit
  3. file_write_branch — k3s/staging/<name>/namespace.yaml committed
     on a staging/<name> branch in the infra repo
  4. issue_create — experiment brief (hypothesis + description + stack
     + provisioning log) on the new repo, returns the issue_url

Returns gitea_url, github_url, issue_url, next_steps. The next_steps
string is the exact shell sequence the operator runs locally to
clone, scaffold via local-dev 'task new-project', and push.

Idempotency: create_project_from_template + repo_mirror_push +
file_write_branch all return JSON-RPC code -32003 (Conflict) when
their target already exists; the orchestrator swallows the conflict
and continues. Re-running on an existing repo restates the brief in
a fresh issue.

Error handling: on any non-conflict downstream failure the response
returns {reached: ["<step>",...], failed_step: "<step>"} alongside
a JSON-RPC error. No rollback — partial state stays so the operator
can resume manually.

New env vars (all optional except GITEA_MCP_URL):
  GITEA_MCP_URL    enables the tool
  GITEA_MCP_TOKEN  bearer auth for gitea-mcp
  GITEA_OWNER      default mathias
  GITHUB_OWNER     default mathiasb
  INFRA_REPO       default infra
  GITHUB_PAT       repo scope, used as mirror remote_password; never logged

Without GITEA_MCP_URL set, the tool is not registered and the
routing pod starts normally (degrades open).

internal/mcpclient/: new minimal JSON-RPC tools/call client with
bearer auth, used by project_create. Unwraps MCP's
content[0].text envelope and surfaces typed errors via mcpclient.Error.

Tests: table-driven against an httptest fake gitea-mcp covering happy
path (4-step success + correct PATCH-style arg shapes), idempotent
repo-exists, mirror failure (partial-success response with reached=
[create_repo] + failed_step=mirror), infra-commit failure (reached up
to mirror + failed_step=infra_commit), and validation errors.

Closes #10
This commit is contained in:
Mathias
2026-05-18 11:44:02 +02:00
parent 7baf8d7e7a
commit 3b79311fdd
7 changed files with 850 additions and 0 deletions

View File

@@ -25,6 +25,16 @@ type RoutingConfig struct {
RouteLocalFloor float64 // HYPERGUILD_ROUTE_LOCAL_FLOOR, default 0.90
RouteLocalCeil float64 // HYPERGUILD_ROUTE_LOCAL_CEIL, default 0.70
PassRateTTLSeconds int // HYPERGUILD_PASS_RATE_TTL_SECONDS, default 60
// project_create configuration. Empty GiteaMCPURL disables the
// project_create tool registration so the routing pod still starts
// in environments where it's not wired up.
GiteaMCPURL string // GITEA_MCP_URL, e.g. http://koala:30340/mcp
GiteaMCPToken string // GITEA_MCP_TOKEN, bearer for gitea-mcp
GiteaOwner string // GITEA_OWNER, default mathias
GitHubOwner string // GITHUB_OWNER, default mathiasb
InfraRepo string // INFRA_REPO, default infra
GitHubPAT string // GITHUB_PAT, repo scope; never logged
}
func LoadRouting() (RoutingConfig, error) {
@@ -56,6 +66,13 @@ func LoadRouting() (RoutingConfig, error) {
}
cfg.PassRateTTLSeconds = ttl
cfg.GiteaMCPURL = os.Getenv("GITEA_MCP_URL")
cfg.GiteaMCPToken = os.Getenv("GITEA_MCP_TOKEN")
cfg.GiteaOwner = envOr("GITEA_OWNER", "mathias")
cfg.GitHubOwner = envOr("GITHUB_OWNER", "mathiasb")
cfg.InfraRepo = envOr("INFRA_REPO", "infra")
cfg.GitHubPAT = os.Getenv("GITHUB_PAT")
return cfg, nil
}