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
91 lines
3.1 KiB
Go
91 lines
3.1 KiB
Go
// Package project implements the `project_create` MCP tool: a single-call
|
|
// pipeline that creates a Gitea repo from a template, configures push-mirror
|
|
// to GitHub, commits a staging namespace manifest to the infra repo, and
|
|
// opens an experiment-brief issue on the new repo. See hyperguild gitea
|
|
// issue #10 for the design.
|
|
package project
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/mathiasbq/supervisor/internal/mcpclient"
|
|
"github.com/mathiasbq/supervisor/internal/registry"
|
|
)
|
|
|
|
// Config holds the orchestration dependencies for the project skill.
|
|
type Config struct {
|
|
// Client talks to the gitea-mcp server. project_create makes 4 sequential
|
|
// calls (create_project_from_template, repo_mirror_push, file_write_branch,
|
|
// issue_create) through this client.
|
|
Client *mcpclient.Client
|
|
|
|
// GiteaOwner is the org/user that owns the new repo and the infra repo
|
|
// the namespace manifest is committed to (typically "mathias").
|
|
GiteaOwner string
|
|
|
|
// GitHubOwner is the GitHub org/user the push-mirror targets
|
|
// (typically "mathiasb").
|
|
GitHubOwner string
|
|
|
|
// GitHubPAT is the personal access token used as the push-mirror
|
|
// password. Must have `repo` scope. Never logged.
|
|
GitHubPAT string
|
|
|
|
// InfraRepo is the name of the infra repo on Gitea where the
|
|
// k3s/staging/<name>/namespace.yaml manifest gets committed
|
|
// (typically "infra").
|
|
InfraRepo string
|
|
}
|
|
|
|
// Skill exposes project_create as an MCP tool.
|
|
type Skill struct{ cfg Config }
|
|
|
|
// New constructs the project Skill.
|
|
func New(cfg Config) *Skill { return &Skill{cfg: cfg} }
|
|
|
|
// Name returns the skill identifier.
|
|
func (s *Skill) Name() string { return "project" }
|
|
|
|
// Tools returns the MCP tool definitions for this skill.
|
|
func (s *Skill) Tools() []registry.ToolDef {
|
|
schema, _ := json.Marshal(map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{
|
|
"type": "string",
|
|
"pattern": `^[a-z][a-z0-9-]{1,38}[a-z0-9]$`,
|
|
"description": "Lowercase repo name. 3-40 chars, must start with a letter.",
|
|
},
|
|
"description": map[string]any{"type": "string"},
|
|
"hypothesis": map[string]any{"type": "string"},
|
|
"folder": map[string]any{
|
|
"type": "string",
|
|
"description": "Informational only — appears in next_steps. Example: AGENTS, AI, QKX.",
|
|
},
|
|
"stack": map[string]any{
|
|
"type": "string",
|
|
"enum": []string{"go-agent", "go-web"},
|
|
"description": "Selects template-go-agent or template-go-web.",
|
|
},
|
|
"private": map[string]any{"type": "boolean"},
|
|
},
|
|
"required": []string{"name", "description", "hypothesis", "stack"},
|
|
})
|
|
return []registry.ToolDef{
|
|
{
|
|
Name: "project_create",
|
|
Description: "Bootstrap a new project: Gitea repo from template, GitHub push-mirror, staging namespace manifest, experiment-brief issue. Idempotent — re-running with an existing repo returns the existing URLs.",
|
|
InputSchema: schema,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handle dispatches the tool call.
|
|
func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
|
if tool != "project_create" {
|
|
return nil, errUnknownTool(tool)
|
|
}
|
|
return s.handleCreate(ctx, args)
|
|
}
|