100 Commits
Author SHA1 Message Date
mathiasandClaude Opus 4.8 93c32edbb5 feat(tools): make repo the canonical repo-identifier arg; name now an alias (#38)
CD / Deploy via GitOps (push) Has been skipped
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 21s
Every caller (claude.ai connector, LLMs primed on gitea/GitHub) sends the repo
identifier as `repo`, but the 33 per-repo identifier tools advertised `name`.
The v0.2.8 shim aliased repo->name so it worked, yet the advertised inputSchema
still said `name` — a misleading contract, with the shim load-bearing.

- Flip all 33 identifier tools: schema property + required + struct field/tag
  from `name` to `repo`. A compliant `repo` caller now matches the struct
  directly; the shim is pure back-compat.
- normalizeAliases is now bidirectional (name<->repo), so legacy `name` callers
  still resolve, and repo_create / create_project_from_template — whose `name`
  means "name of the NEW repo", not an existing-repo id — keep `name` and still
  accept `repo`.
- `number`/`index` left as-is (out of scope; separate pre-existing quirk where
  pr_merge advertises `index` rather than `number`).
- Tests: schema-canonical assertion + flipped alias round-trip (explicit `repo`
  wins over `name`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:42:18 +02:00
mathiasandClaude Opus 4.8 4ebea7d023 fix(workflow_run_trigger): 204 dispatch is success; resolve run via listing (#41)
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 21s
CD / Deploy via GitOps (push) Has been skipped
Gitea's workflow_dispatch endpoint returns 204 No Content with no Location
header. DispatchWorkflow required that header, so every successful dispatch
errored with "missing Location header" and never yielded a run ID — making
the tool unusable for CAD dispatch.

- DispatchWorkflow now returns error-only; 204 = success, no Location needed.
  Body already carried ref+inputs; kept and covered by test.
- Tool snapshots the newest existing workflow_dispatch run before dispatch,
  then polls ListWorkflowRuns after and returns the newest run with ID above
  that baseline (avoids returning a stale prior run). Falls back to an honest
  "dispatched, run not yet registered" result rather than failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:12:45 +02:00
mathiasandClaude Opus 4.8 711dc46e5e feat(create_project): add dispatch_allow to inject .dispatch-allow (#43)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Has been skipped
Optional dispatch_allow bool (default false). When true, inject a
.dispatch-allow file at repo root on the resolved default branch after
substitution, marking the new project dispatch-eligible (dispatch#3)
without a manual follow-up commit.

Rides the existing upsertRetry path so injection inherits the infra#179
branch-readiness / partial_failure handling; a stalled injection degrades
exactly like substitution. Reported in files_substituted. false/omitted
is byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:57:39 +02:00
mathiasandClaude Opus 4.8 039598855c fix(create_project): fast raw create + honest partial_failure (#42, infra#179)
CD / Lint / Test / Vet (push) Successful in 8s
CD / Build & Import (push) Successful in 21s
CD / Deploy via GitOps (push) Successful in 6s
gitea's template-generate is slow-async on this instance (repo not writable for
>40s; infra#179) — no synchronous MCP tool can wait that long, and the 60s retry
made the call hang until the client timed out. Bound the write-readiness retry to
5s (a healthy gitea commits in ~1s and this still catches it), and when the branch
isn't writable in time, return a clear partial_failure: repo created, substitution
deferred, finalize locally with `hyperguild new-project`. Substitution logic is
intact and completes automatically once generate is fast (infra#179). Tool
description updated to describe substitution as best-effort. Refs #42, infra#179.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:38:13 +02:00
mathiasandClaude Opus 4.8 d45ba712ce fix(create_project): make the write the branch-readiness gate (#42)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Deploy via GitOps (push) Successful in 4s
CD / Build & Import (push) Successful in 22s
BranchExists returns true before the generated branch is writable, so
waitForBranch didn't help and a 2.5s retry budget was too short (branch became
writable ~30s post-generate under load in live testing). Drop waitForBranch;
let upsertRetry be the gate — retry the write on the transient "branch does not
exist" not-found for up to 60s, early-exit on success. Once the first write
lands the branch is writable and the rest succeed immediately. Refs #42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:17:11 +02:00
mathiasandClaude Opus 4.8 4d658004ae fix(create_project): handle gitea generate-async branch race (#42)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Successful in 4s
Live e2e surfaced a race unit tests couldn't (mocks are instant): gitea's
/generate returns and serves reads before the branch ref is writable, so the
first content writes 404 "branch does not exist" for a beat — aborting the
whole substitution pass. Add waitForBranch (poll BranchExists after generate)
+ upsertRetry (retry writes on the transient not-found). Test fake now serves
the branch readiness probe. Refs #42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:11:49 +02:00
mathiasandClaude Opus 4.8 3329ff3088 fix(deps): migrate mcp-chassis to git.d-ma.be path — unblock CD build (#74)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Successful in 4s
The image build's `go mod download` failed on the stale
gitea.d-ma.be/mathias/mcp-chassis import (the gitea→git rename; the server no
longer serves a matching go-import meta tag). This is the latent #74 breakage
flagged for gitea-mcp, triggered by the first clean rebuild since the rename
(the #42 push). Point at git.d-ma.be/mathias/mcp-chassis v0.2.0 + go mod tidy.

Unblocks deploying the #42 create_project_from_template fix. gitea-mcp's own
module path stays gitea.d-ma.be (main module, not fetched — separate cleanup).
Refs #74, #42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:06:35 +02:00
mathias 2ebaee8d03 chore: re-sync context adapters from canonical AGENT.md
CD / Lint / Test / Vet (push) Successful in 8s
CD / Build & Import (push) Failing after 5s
CD / Deploy via GitOps (push) Has been skipped
Derived adapters drifted after the root ~/dev/.context/AGENT.md gained the
rule-0 pre-task ritual; task check's context:check gate failed on it
(pre-existing, unrelated to #42). Regenerate via context-sync.sh.
2026-07-01 22:53:15 +02:00
mathias e30951ad72 fix(create_project): use fmt.Fprintf in test fake (lint QF1012)
Follow-up to e3cdd23 — staticcheck QF1012 flagged w.Write([]byte(fmt.Sprintf(...)))
in the rewritten test's fake server. Behaviour unchanged; lint gate green.
2026-07-01 22:53:15 +02:00
mathiasandClaude Opus 4.8 e3cdd23260 fix(create_project): substitute the whole tree, rename cmd dir, resolve branch (#42)
CD / Lint / Test / Vet (push) Failing after 5s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
create_project_from_template returned files_substituted:null and produced a
non-building scaffold. Three root causes, all fixed:

1. Empty branch: /generate omits default_branch, so every SubstituteFile read
   hit an empty ref and 404'd → nothing substituted. Resolve the branch
   explicitly (re-fetch the repo; fall back to "main"). The old unit test hid
   this by mocking default_branch:"main".
2. Incomplete + rename-incapable: substitution ran over a fixed 6-file list
   that missed cmd/__PROJECT_NAME__/main.go and could not rename the
   cmd/__PROJECT_NAME__/ directory. Replace with a recursive tree walk:
   content-substitute every blob, and for any path carrying a placeholder,
   rename it (POST-create new path + delete old).
3. Stale module host: __MODULE_PATH__ used gitea.d-ma.be (the pre-rename host,
   which breaks `go mod download` downstream). Use git.d-ma.be.

Also: fail loud — if nothing was substituted, populate partial_failure instead
of returning silent success (the null that started this).

Tests rewritten to drive the tree-walk flow and assert: cmd/ rename (new path
POST + old path delete), git.d-ma.be module substitution, empty-generate-branch
fallback, and the loud-on-nothing path.

Closes #42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:39:48 +02:00
mathiasandClaude Opus 4.8 184d5a95dd fix(tools,gitea): alias repo/index args and reject empty path segments
CD / Deploy via GitOps (push) Has been skipped
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 23s
Per-repo MCP tools 404'd while owner-level tools worked (#36). Root cause was
a parameter-name contract mismatch, not a routing fault: every per-repo tool
declares the repo identifier as 'name' (and issue/PR index as 'number'), but
every caller — the claude.ai connector, LLMs primed on gitea's own API — sends
'repo' and 'index'. The unmatched fields zero-valued the upstream path segment,
producing '/api/v1/repos/{owner}//...', which gitea answers with its generic
api-404 whose body points at /api/swagger. That swagger pointer is gitea boiler-
plate, not a misroute — the MCP dispatch was correct all along.

Two layers of defence:
- parseArgs aliases repo->name and index->number (explicit canonical wins;
  alias key left intact so pr_merge's real 'index' field is unaffected). Kills
  the recurrence by accepting the idiomatic argument names.
- the gitea client rejects any path with an empty segment before the HTTP call,
  returning ErrValidation instead of forwarding a malformed path and surfacing
  gitea's opaque swagger-404. Kills the silent-misleading-error class.

Closes #36

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:16:33 +02:00
mathiasandClaude Opus 4.8 e2594f45f2 refactor(tools): extract RegisterAll shared tool registration
main.go and tests now share one registration list so a tool wired in one
place cannot silently go missing from the other. Adds a dispatch round-trip
test asserting every registered tool resolves and ships a parseable schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:52:29 +02:00
mathias f99a8b38cb fix(ci): quote "on" key so Gitea parses workflow triggers
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Failing after 0s
CD / Deploy via GitOps (push) Has been skipped
Bare on: parses as YAML boolean true (Norway problem); Gitea then ignores the triggers and silently skips jobs. Quoting forces the string key.
2026-06-03 08:38:46 +02:00
mathias 8a751741a4 feat: add issue_edit tool (#34)
CD / Lint / Test / Vet (push) Successful in 8s
CD / Build & Import (push) Failing after 0s
CD / Deploy via GitOps (push) Has been skipped
Adds issue_edit, mapping to Gitea's PATCH /repos/{owner}/{repo}/issues/{index},
so an existing issue's title and/or body can be edited through the MCP surface.
Previously the only post-create mutation was issue_comment, which buries
backlinks in the thread instead of the canonical body.

Partial patch via pointer fields (omitempty): omitted fields are left
untouched, an explicit empty string clears a field. Body is sent verbatim —
no identity footer — so repeated edits are idempotent, matching the acceptance
criteria. Registered alongside the other issue tools for tool_search discovery.

Closes #34
2026-06-02 16:11:33 +02:00
mathiasandClaude Opus 4.7 668e8fa28d feat: /healthz reports JWT validator status (refs #6)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 20s
CD / Deploy via GitOps (push) Successful in 4s
/healthz now returns JSON with three-state JWT status: disabled
(DEX_ISSUER_URL unset), enabled (validator initialized), or
degraded (configured but init failed — only static-token auth
currently accepted). last_error surfaces the init failure so ops
can correlate with Dex outage windows.

Partial fix for #6. The cited internal/auth/jwt.go moved out
of this repo in 658f4ba (mcp-chassis migration); per-attempt
logging and 503 + WWW-Authenticate temporarily_unavailable
require chassis-side changes and a coordinated v0.1.1 bump
across all MCP consumers — tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:55:09 +02:00
mathiasandClaude Opus 4.7 3c1ca7d3db feat: add issue_list_comments tool (closes #32)
CD / Lint / Test / Vet (push) Successful in 17s
CD / Build & Import (push) Successful in 19s
CD / Deploy via GitOps (push) Successful in 4s
Lists all comments on an issue or PR via GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments.
Read-only, allowlist-gated. Extended IssueComment struct with user/timestamps populated by the list endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:01:34 +02:00
mathiasandClaude Opus 4.7 8bea0d2f27 chore: remove stray cd.yml.notes file from CI retrigger commit
CD / Lint / Test / Vet (push) Successful in 8s
CD / Build & Import (push) Successful in 19s
CD / Deploy via GitOps (push) Successful in 4s
The file was an accident in commit 24c3533 — meant as a tmp marker,
should have been removed before commit. Harmless but trash. Removing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:26:35 +02:00
mathiasandClaude Opus 4.7 24c353383f ci: retrigger build after chassis repo made public
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 22s
CD / Deploy via GitOps (push) Successful in 5s
mcp-chassis was created private on 2026-05-22 then ported here in
commit 658f4ba, which caused CI Build to fail when go mod download
hit the chassis URL and got prompted for credentials. The chassis is
now public (Gitea repo flipped via API). No code change needed; this
empty commit retriggers the build pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:17:54 +02:00
mathiasandClaude Opus 4.7 be85baf410 fix(ci): allow Dockerfile build to fetch internal gitea modules
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Failing after 5s
CD / Deploy via GitOps (push) Has been skipped
mcp-chassis (added in commit 658f4ba) is hosted at gitea.d-ma.be, and
Gitea returns http:// in its go-import meta tag. Default go module
resolution goes through proxy.golang.org (which can't reach internal
hosts) and falls back to direct git, which gets the http:// URL and
refuses it.

Fix:
- GOPRIVATE=gitea.d-ma.be — skip proxy.golang.org
- GOPROXY=direct — direct git, no proxy attempt
- GOSUMDB=off — bypass sumdb (also doesn't know internal modules)
- git config insteadOf rewrites http:// → https:// for gitea.d-ma.be

Without this, gitea-mcp CI Build & Import failed on the chassis port
(sha=658f4ba). Re-running CI should now succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:12:33 +02:00
mathiasandClaude Opus 4.7 658f4ba84f feat(auth): migrate to gitea.d-ma.be/mathias/mcp-chassis v0.1.0
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Failing after 2s
CD / Deploy via GitOps (push) Has been skipped
First real port of the MCP chassis library — abort-criterion check for
spike S3 of the 2026-05 homelab architecture review.

Changes:
- Drop internal/auth/jwt.go (~79 LOC) — chassis provides JWTValidator
  with identical signature.
- Drop internal/auth/bearer.go (~42 LOC) — chassis BearerMiddleware
  has the same static-or-JWT semantics plus an optional WWW-Authenticate
  resource_metadata challenge (consumed via new resourceMetadataURL arg).
- Drop internal/auth/bearer_test.go — same scenarios are covered in
  the chassis bearer_test.go now.
- main.go: import chassis as `chassisauth`, build resourceMetadataURL
  only when both DexIssuerURL + MCPResourceURL are set, replace the
  inline /.well-known/oauth-protected-resource handler with the chassis
  ProtectedResourceHandler.

internal/auth/caller.go (oauth2-proxy header → context) stays — chassis
out-of-scope.

Net LOC change: -~150 LOC duplicated infra + a 5-LOC import.
go.mod gains gitea.d-ma.be/mathias/mcp-chassis v0.1.0 (jwx/v2 + testify
already transitive, no new top-level deps).

Verifies abort criterion: one PR, one binary's worth of port, task check
green (lint + test + vet + govulncheck clean). Per the S3 spike spec,
this clears the chassis to continue. Next port: hyperguild/ingestion
(brain-mcp), filed as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:25:23 +02:00
mathias 60212fc5d2 feat: issue_list + workflow_run_list tools (#28, #29)
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Has been skipped
Adds the *_list partners that the existing *_get tools have been
missing. Same pattern as repo_list — owner allowlisted, capLimit
helper for pagination, next_page surfaced when the page is full.

internal/gitea/issues.go:
- ListIssues(owner, repo, args) hitting
  GET /api/v1/repos/{owner}/{repo}/issues with type=issues server-side
  so PRs don't leak in (gitea conflates them on this endpoint).
- ListIssuesArgs struct: State, Labels, Since (ISO 8601), Page, Limit.

internal/gitea/workflows.go:
- ListWorkflowRuns(owner, repo, args) hitting
  GET /api/v1/repos/{owner}/{repo}/actions/runs.
- Expanded WorkflowRun struct with DisplayTitle, Event, HeadSHA,
  HeadBranch, WorkflowID, RunNumber, UpdatedAt, Actor so callers
  can pin runs to a commit / branch without a second lookup.
- ListWorkflowRunsArgs: Branch, HeadSHA, Status, Event, Workflow,
  Page, Limit. Status/Event 'all' treated as no-filter.

internal/tools/issue_list.go:
- Default state=open, default limit=30 (matches repo_list).
- next_page returned only when len(issues) == limit.

internal/tools/workflow_run_list.go:
- Default limit=10 (most common use is 'what just happened',
  not paging).
- Returns runs + total + optional next_page.

Tests: table-driven for both — happy path, empty result, filter
combinations, allowlist rejection. workflow_run_list also asserts
the 'status=all is no-op' behavior (no query param emitted).

Closes #28
Closes #29
2026-05-18 08:06:11 +02:00
mathias dc907fb7e0 feat: issue_close + issue_reopen tools (#30)
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Has been skipped
Adds two MCP tools that PATCH /api/v1/repos/{owner}/{name}/issues/{number}
with {"state":"closed"} or {"state":"open"}. Both use a shared
SetIssueState helper on the gitea client.

- internal/gitea/issues.go: SetIssueState method using the existing
  PatchJSON + MapStatus + json.Unmarshal pattern from GetIssue.
- internal/tools/issue_close.go: IssueClose tool. owner+name+number
  args. Owner allowlist enforced. Returns the updated issue. Reversible
  via issue_reopen, classified LOW risk.
- internal/tools/issue_reopen.go: mirror of IssueClose with
  state="open". Same risk profile.
- Registered both tools in cmd/gitea-mcp/main.go.
- Tests for both: success (asserts PATCH method, path, body), 404,
  and allowlist rejection — same shape as issue_get_test.go.

Closes #30
2026-05-18 07:51:17 +02:00
mathias c4bd3396c4 chore: re-sync context adapters from updated root AGENT.md 2026-05-18 07:51:17 +02:00
mathiasandClaude Opus 4.7 11f86f5d99 chore: adopt trunk-based development
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 3s
Closes #27.

PROJECT.md
- Git section: TBD as the convention. Commit to main, one logical
  change per commit, `task check` locally before push, CI is the
  quality gate. PRs only for the parallel-agent exception.
- Agent rule 6: rewritten to match.

.gitea/workflows/cd.yml
- Drop the pull_request trigger — vestigial under TBD.
- Drop the `if: github.event_name != 'pull_request'` guard on the
  build job (now always true since pull_request no longer fires).
  Tag pushes still build (no version gating regression).
- Deploy `if` left alone — already correctly limits deploy to
  main pushes, skipping tag-push builds.

.githooks/pre-push (new)
- Runs `task check` before every push. Set up via `task setup:hooks`,
  which sets core.hooksPath to the in-repo .githooks dir.

Taskfile.yml
- New `setup:hooks` task to install the pre-push hook on a fresh
  clone.

README.md
- Quickstart section showing `task setup:hooks` + the TBD policy.

Derived adapters regenerated via `task context:sync` and committed
in the same commit (single-commit invariant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:44:52 +02:00
mathias f7076c9ac8 docs: mark v0.2 complete, set next-up context for v0.2.5
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 4s
2026-05-17 09:26:47 +02:00
mathiasandClaude Opus 4.7 3cccbfb8cb chore: re-sync context adapters after rebase
CD / Lint / Test / Vet (pull_request) Successful in 7s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
Upstream .context/PROJECT.md gained a branch-protection rule + an
extra agent instruction. Pure regeneration via scripts/context-sync.sh
to make task check pass before force-push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:02:08 +02:00
mathias eeefc626ed feat(repo_update): tool for archiving + metadata patches
Adds a repo_update tool exposing PATCH /api/v1/repos/{owner}/{name}
with optional pointer fields (archived, description, private,
website, template). Only fields set by the caller are sent on the
wire, so the server patches exactly what was asked for.

Originally needed to archive ingestion-svc cleanly instead of
leaving a README tombstone, and to flip template-go-{agent,web}
to template=true so create_project_from_template stops failing
the "is not marked as template" guard.

Wire-level enforcement of "at least one field" returns ErrValidation
before any network call, preventing no-op PATCHes.

private=false (making a repo public) is allowed but flagged in the
tool description with a "verify intent before calling" warning.
The earlier issue draft suggested an ntfy confirmation hook for
that path — out of scope for this PR; the warning string is the
minimum that fits inside the tool surface today.

Wires NewRepoUpdate into cmd/gitea-mcp/main.go alongside the rest
of the repo_* family.

Closes #12
2026-05-16 23:54:16 +02:00
mathias 5545d6ab4b fix(create_project_from_template): accept per-call template_name override
The template name was hardcoded into the binary at startup via
NewCreateProjectFromTemplate("mathias", "template-go-web"), so
generating from a different template (e.g. template-go-agent)
required a code change and restart. The constructor already
parameterised it correctly — the gap was at the tool's input
schema, which never exposed template_name to the caller.

Adds an optional template_name input field. When set, it overrides
the server-configured default for that call only; when omitted,
behavior is unchanged. Template owner stays server-configured —
only the repo name is per-call.

Server-side validation already verifies the resolved template
exists and is marked as a template repo, so no enum constraint
is added — keeps the door open for future templates (go-ml,
go-service, ...) without redeploys.

Adds TestCreateProjectTemplateNameOverride verifying the override
directs both the template lookup and the /generate POST.

Closes #24
2026-05-16 23:24:16 +02:00
mathias 9013c8ff9c fix(pr_files_diff): copy per-file diff bytes to break buffer aliasing
splitUnifiedDiff used bytes.Buffer to accumulate each file's diff,
then stored buf.Bytes() into the result map and called buf.Reset()
to start the next file. bytes.Buffer.Bytes() returns the buffer's
internal backing slice; Reset() resets length to 0 but reuses the
same backing array. As a result, every map entry aliased the same
storage, so all files ended up showing the LAST file's diff content.

Fix: copy the bytes into a fresh slice before storing in the map.

Adds TestPRFilesDiffPerFileIsolation as a regression test that
asserts each file entry contains its OWN diff --git header and
none of the other files' headers. Verified failing on the prior
code, passing after the fix.

Closes #25
2026-05-16 23:24:16 +02:00
mathiasandClaude Opus 4.7 f26f922c96 chore: re-sync context adapters with upstream root
Derived adapters drifted from canonical root .context/AGENT.md after
the pgvector default change landed upstream. Pure regeneration via
scripts/context-sync.sh, no manual edits. Required to make task check
pass before the feature commits on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:16 +02:00
mathias d4dddbdb6c feat(tools): issue_get, release_create, repo_delete (#11, #17, #20)
CD / Build & Import (pull_request) Has been skipped
CD / Lint / Test / Vet (pull_request) Successful in 7s
CD / Deploy via GitOps (pull_request) Has been skipped
issue_get: GET /repos/{owner}/{repo}/issues/{number} — full issue with labels, assignees, comment count
release_create: POST /repos/{owner}/{repo}/releases — create release and tag in one call
repo_delete: DELETE /repos/{owner}/{repo} — confirm=<repo name> required, blocks accidents
2026-05-15 13:59:06 +02:00
mathias 5f3ad99122 feat(tools): repo_tree, repo_topics_update, file_read dir fix (#14, #15, #18)
CD / Lint / Test / Vet (pull_request) Successful in 7s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
repo_tree: GET /git/trees/{ref}?recursive=1 — full recursive file tree
repo_topics_update: PUT /repos/{owner}/{repo}/topics — replace topic list
file_read: detect array response and return descriptive error for dir paths
2026-05-15 10:23:31 +02:00
mathias e2da495581 feat(tools): add repo_create, repo_update, repo_mirror_push (#12, #13, #16)
CD / Lint / Test / Vet (pull_request) Successful in 7s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
repo_create: POST /user/repos or /orgs/{org}/repos, is_org flag routes
repo_update: PATCH /repos/{owner}/{repo}, confirm required when private=false
repo_mirror_push: add/list/delete push mirrors, password never returned
2026-05-15 10:14:18 +02:00
mathias 7178ae32be chore: re-sync context adapters 2026-05-15 09:53:09 +02:00
mathiasandClaude Opus 4.7 174669b9f6 fix(mcp): drop strict session-id requirement on POST /mcp
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 48s
The claude.ai connector's MCP transport proxy does not reliably
propagate the Mcp-Session-Id header issued during initialize. With the
previous strict gate (return 400 plain text "missing or invalid
Mcp-Session-Id"), every tools/list and tools/call from claude.ai
failed and the Anthropic proxy surfaced it as:

  Streamable HTTP error: {"jsonrpc":"2.0","id":N,"error":
    {"code":-32600,"message":"Anthropic Proxy: Invalid content from server"}}

— because the plain-text 400 response is not valid JSON-RPC.

All tools the gitea-mcp server exposes are stateless single-shot
calls, so there is no functional reason to gate them on a session.
brain-mcp and supervisor-mcp don't gate either, and claude.ai works
against them fine. Match that behavior: keep issuing Mcp-Session-Id
on initialize for clients that want to use it, but stop rejecting
calls that don't send one back.

Test renamed PostWithoutSessionRejected → PostWithoutSessionAccepted
and updated to assert the tools/list response shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:58:13 +02:00
mathias 7a53935a9e chore(mcp): remove supervisor MCP entry
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 13s
CD / Deploy via GitOps (push) Failing after 1m3s
2026-05-12 14:49:35 +02:00
mathiasandClaude Opus 4.7 3795800461 fix(auth): require Bearer on /mcp regardless of DefaultToken
CD / Lint / Test / Vet (push) Successful in 7s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 4s
Previously BearerMiddleware allowed requests with no Authorization
header to pass through whenever GITEA_MCP_DEFAULT_TOKEN was set. The
intent was "fall back to the service PAT for upstream Gitea calls,"
but the side effect was that anyone could hit /mcp anonymously and the
server would happily proxy requests as the service account.

Drop that path. Auth on /mcp now requires either:
  - a valid Dex-issued JWT, or
  - a Bearer matching GITEA_MCP_STATIC_TOKEN.

The Gitea service PAT (GITEA_MCP_DEFAULT_TOKEN) is no longer wired
into BearerMiddleware at all — it stays an upstream-client concern,
used by gitea.NewClient for outbound API calls only. This decouples
"can this caller invoke a tool" from "what credentials does the tool
use against Gitea".

Tests updated: drop the NoAuthHeader_WithDefault permissive case, add
NoAuthHeader_RejectsEvenWhenStaticConfigured to lock in the new
behavior.

Closes part of mathias/infra#2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:44:38 +02:00
mathias 9987522f1a fix(ci): skip context sync drift check in CI environment
CD / Lint / Test / Vet (push) Successful in 8s
CD / Build & Import (push) Successful in 13s
CD / Deploy via GitOps (push) Successful in 4s
Context-sync walks up the directory tree to find the root AGENT.md.
On koala's act_runner, checkout is under /var/lib/act_runner/, not
under ~/dev/, so ROOT_CONTEXT resolves to empty. Generated files
differ from committed files (which include root context), causing
the drift check to fail.

Skip context sync when CI=true; local checks still verify sync.
2026-05-12 12:09:24 +02:00
mathias 0e53738d9f ci: retrigger after deps fix
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
2026-05-12 11:40:53 +02:00
mathias 91be18c100 feat(auth): JWT-or-static middleware + /.well-known/oauth-protected-resource (issue #5)
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
- internal/auth/jwt.go: JWTValidator via lestrrat-go/jwx/v2, JWKS auto-refresh
- internal/auth/bearer.go: replace Gitea PAT validation with JWT->static->default chain
- internal/gitea/client.go: always use service PAT; remove TokenFromContext lookup
- internal/config/config.go: add DexIssuerURL, MCPAudience, MCPResourceURL, StaticToken
- cmd/gitea-mcp/main.go: wire validator, fix /.well-known to return real AS list
- bearer_test.go: rewrite for new API
2026-05-12 11:30:52 +02:00
mathiasandClaude Sonnet 4.6 efbbd37882 chore: remove debug request logging
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 11s
CD / Deploy via GitOps (push) Successful in 2s
Root cause confirmed (claude.ai sends no auth header); fallback token
is in place. Logging no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:30:08 +02:00
mathiasandClaude Sonnet 4.6 9d08352324 feat(auth): fall back to GITEA_MCP_DEFAULT_TOKEN when no Bearer header
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 11s
CD / Deploy via GitOps (push) Successful in 3s
claude.ai connectors call the server with no Authorization header (confirmed
via request logging). Add a configurable default Gitea PAT so unauthenticated
clients (like claude.ai) can still reach the server.

Claude Code continues to pass per-request PATs; defaultToken="" preserves
the existing strict behaviour when the env var is unset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:22:04 +02:00
mathiasandClaude Sonnet 4.6 70173875d8 debug: add request logging to diagnose claude.ai connector auth
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 3s
Logs method, path, origin, has_auth, user_agent per request so we can
see exactly what claude.ai sends. Temporary; remove once root cause found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:41:50 +02:00
mathiasandClaude Sonnet 4.6 3784bcc31b fix(lint): check resp.Body.Close error in bearer_test.go
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 15s
CD / Deploy via GitOps (push) Successful in 2s
Silences errcheck violations that have been breaking CI since the test
was written.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:25:37 +02:00
mathiasandClaude Sonnet 4.6 f63605bdd0 fix(mcp): downgrade protocolVersion to 2025-03-26
CD / Lint / Test / Vet (push) Failing after 3s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
Claude Code CLI rejects 2025-06-18 and silently drops the connection;
2025-03-26 is the highest version it supports. Fixes #4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 07:53:18 +02:00
mathias c4d3735272 fix(mcp): allow GET/SSE without session ID for claude.ai compatibility
CD / Lint / Test / Vet (push) Failing after 3s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
2026-05-07 23:22:21 +02:00
mathias d8db786e27 ci: add environment: staging gate to deploy job
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
Aligns with cobalt-dingo reference — the deploy job was missing the
Gitea Actions environment protection so staging approvals/secrets were
not enforced.
2026-05-07 21:52:40 +02:00
mathias 923689afa5 feat: replace static API token with per-request Gitea PAT pass-through
Callers now supply their own Gitea PAT as a Bearer token; the server validates
it against GET /api/v1/user and threads it through context to all downstream
Gitea API calls. GITEA_API_TOKEN env var and the GiteaAPIToken config field are
removed.
2026-05-07 21:04:47 +02:00
mathias 9a5d0005c5 feat: add 9 GitOps agent tools for full GitOps loop
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 11s
CD / Deploy via GitOps (push) Has been skipped
Adds branch_list, branch_delete, branch_protection_get, pr_list,
pr_merge, dir_list, file_delete, tag_create, and repo_status so an
AI agent can autonomously drive feature-branch or trunk-based
development workflows against Gitea.
2026-05-07 08:11:45 +02:00
mathiasandClaude Sonnet 4.6 c0576359d7 feat: register 9 new GitOps tools in main
Wires branch_list, branch_delete, branch_protection_get, pr_list,
pr_merge, dir_list, file_delete, tag_create, and repo_status into the
MCP server registry so they are discoverable and callable by agents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:00:29 +02:00
mathias 0c5903a196 feat(tools): repo_status 2026-05-06 22:59:51 +02:00
mathias 839fc93dcd feat(tools): tag_create 2026-05-06 22:54:22 +02:00
mathias 5dac4856bd feat(tools): file_delete 2026-05-06 22:51:21 +02:00
mathias 0eb9ebcafd feat(tools): dir_list 2026-05-06 22:49:50 +02:00
mathias 284d5e19f6 feat(tools): pr_merge 2026-05-06 22:48:02 +02:00
mathias 388131c8cd feat(tools): pr_list 2026-05-06 22:46:11 +02:00
mathias ddfcc32afd feat(tools): branch_protection_get 2026-05-06 22:44:24 +02:00
mathias 9e4251c1a7 feat(tools): branch_delete 2026-05-06 22:42:38 +02:00
mathias 06882d185e fix(tools): branch_list schema constraints 2026-05-06 22:41:05 +02:00
mathias 073d88b29a feat(tools): branch_list 2026-05-06 22:38:15 +02:00
mathias 44c42fa636 feat(gitea): add DeleteJSONBody for delete-with-body requests 2026-05-06 22:36:37 +02:00
mathiasandClaude Sonnet 4.6 e7bd954e90 docs: add GitOps agent tools implementation plan
11 tasks covering 9 new tools, client methods, tests, and registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 22:22:41 +02:00
mathiasandClaude Sonnet 4.6 0cd465fb68 docs: add GitOps agent tools design spec
9 new tools to enable full autonomous GitOps loop: repo_status,
branch_list/delete/protection_get, pr_list/merge, dir_list,
file_delete, tag_create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:51:39 +02:00
mathiasandClaude Sonnet 4.6 9cbb564cd9 fix: add OAuth discovery endpoints for claude.ai handshake
CD / Lint / Test / Vet (pull_request) Successful in 5s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
Implements RFC 9728 protected resource metadata and HEAD probe so
claude.ai can complete its pre-handshake discovery without hitting 404.

- GET /.well-known/oauth-protected-resource → 200 {"authorization_servers":[]}
- GET /.well-known/oauth-authorization-server → 404 (no auth server)
- HEAD /mcp → 200 + MCP-Protocol-Version: 2025-06-18 header

Closes #2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:19:14 +02:00
mathias c345025221 fix(lint): staticcheck S1030, QF1002 and remove unused _ctx stub
CD / Lint / Test / Vet (push) Successful in 4s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Has been skipped
2026-05-05 09:02:39 +02:00
mathias 64559f0250 fix(lint): check Body.Close error return in http client
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
2026-05-05 08:55:31 +02:00
mathias b8463d66a0 chore: drop environment: staging (no-op for solo homelab)
CD / Lint / Test / Vet (push) Failing after 2s
CD / Build & Import (push) Has been skipped
CD / Deploy via GitOps (push) Has been skipped
2026-05-05 08:51:17 +02:00
mathias d261a9f3fe feat: gitea actions cd.yml (cobalt-dingo pattern) 2026-05-05 07:52:45 +02:00
mathias 09b3f25211 chore: add check task for CI 2026-05-05 07:52:39 +02:00
mathias a8d04242d9 feat: dockerfile 2026-05-05 07:52:29 +02:00
mathias 1f9934349b chore(tools): centralize pagination cap helper 2026-05-04 23:06:38 +02:00
mathias 4274b48ea5 feat(gitea): default-branch lru cache
Shared LRU avoids repeated Gitea calls for default-branch resolution;
the simple stdlib map alternative would race on concurrent access without
a mutex per entry, which is more code than the LRU.
2026-05-04 23:06:06 +02:00
mathias fb473262ba feat(gitea): read retry once on 5xx GET 2026-05-04 23:04:55 +02:00
mathiasandClaude Sonnet 4.6 39dc22ec3a feat(tools): create_project_from_template
Generates a new repo from mathias/template-go-web via Gitea's generate
API, then substitutes __PROJECT_NAME__ and __MODULE_PATH__ placeholders
in six known files (best-effort, partial failure surfaced in result).

Validates name regex, allowlist, template flag, and destination
non-existence before generating. Adds Template field to gitea.Repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 23:02:21 +02:00
mathiasandClaude Sonnet 4.6 e95e87e8e3 feat(tools): pr_files_diff with caps
Returns per-file unified diff for a PR, capped at 20KB/file and 200KB
total response. Files exceeding per-file cap report truncated+omitted_lines;
files that would push the response over 200KB go to omitted_files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:57:11 +02:00
mathiasandClaude Sonnet 4.6 d3d0fed6b1 feat(tools): pr_comment with identity footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:53:14 +02:00
mathiasandClaude Sonnet 4.6 c8a353aa35 feat(tools): issue_comment with identity footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:52:24 +02:00
mathiasandClaude Sonnet 4.6 6f43ff216f feat(tools): issue_create with identity footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:51:40 +02:00
mathiasandClaude Sonnet 4.6 2c6b9986e4 feat(tools): code_search (org-wide fan-out)
When repo is omitted, lists owner's repos then concurrently searches
each one (semaphore cap 5, 5s per-repo timeout). Merges and sorts
hits by score desc with deterministic tiebreak. Partial failures
tracked in partial_repos without aborting the whole fan-out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:48:39 +02:00
mathiasandClaude Sonnet 4.6 e4a9d058f0 feat(tools): code_search (single-repo)
Adds SearchCode to gitea.Client and code_search MCP tool for single-repo
code search via GET /api/v1/repos/{owner}/{repo}/search?type=code.
Fan-out placeholder returns ErrValidation (lands in 7.3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:47:33 +02:00
mathiasandClaude Sonnet 4.6 61cce37ff5 feat(tools): repo_search with allowlist post-filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:44:44 +02:00
mathias 43e016e8fa feat(tools): workflow_run_status 2026-05-04 22:25:23 +02:00
mathias ba172e3db8 feat(tools): workflow_run_trigger 2026-05-04 22:25:10 +02:00
mathias c4874ae8d1 feat(tools): pr_get 2026-05-04 22:21:20 +02:00
mathias 9972dcd94e feat(tools): pr_create with identity footer 2026-05-04 22:20:33 +02:00
mathiasandClaude Sonnet 4.6 5af8addc26 feat(tools): file_write_branch
Add BranchExists/CreateBranch/UpsertFile gitea client methods and the
file_write_branch MCP tool. Branch is auto-created from base (or repo
default_branch) when it doesn't exist; file is upserted via PUT contents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:15:39 +02:00
mathiasandClaude Sonnet 4.6 044086b067 feat(tools): file_read with default-branch resolution
Adds GetFileContents to the gitea client and a file_read MCP tool.
When ref is omitted, the tool resolves the repo default_branch via
GetRepo before fetching contents. Decoded content capped at 1 MiB.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:11:50 +02:00
mathias f10cc9ac4b feat(tools): repo_get 2026-05-04 22:08:24 +02:00
mathias 33ad02d369 feat(tools): repo_list 2026-05-04 22:07:44 +02:00
mathias 4ebb1eef6d feat(allowlist): owner allowlist enforcement 2026-05-04 21:26:50 +02:00
mathias d564cdcae3 feat(gitea): typed error mapping from http status 2026-05-04 21:26:29 +02:00
mathias 2ecffd1b7a feat(gitea): base http client with token auth 2026-05-04 21:26:03 +02:00
mathias 4dba4ca192 feat(main): wire caller middleware into /mcp 2026-05-04 21:20:37 +02:00
mathias 3613c36afb feat(identity): created-content footer 2026-05-04 21:05:42 +02:00
mathias 0b5daca198 feat(auth): X-Auth-Request-User context middleware 2026-05-04 21:05:16 +02:00
mathiasandClaude Sonnet 4.6 c6c328e517 fix(mcp): map tool-not-found to CodeNotFound via registry sentinel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:59:15 +02:00
mathiasandClaude Sonnet 4.6 93c5a6934b feat(mcp): cap inbound request body at 1 MiB
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:58:36 +02:00
mathiasandClaude Sonnet 4.6 ba5068648b refactor(mcp): compose origin allowlist as middleware, remove duplication
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:58:08 +02:00
mathiasandClaude Sonnet 4.6 36765b8360 feat(mcp): streamable HTTP transport with session, init, and dispatch
Implements the Streamable HTTP transport: POST routing handles initialize
(issues session ID), tools/list, tools/call, and unknown methods; GET SSE
emits a keepalive comment then blocks on context cancellation. A minimal
registry stub is introduced so the server compiles and tools/list returns
an empty array until Phase 6+ registers real tools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:49:54 +02:00
mathiasandClaude Sonnet 4.6 50a3b27825 test(mcp): pin session concurrency, document error codes, assert id round-trip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:46:07 +02:00