feat: gitea-rest + brain-rest skills — default to direct REST API on CLI to bypass MCP-relay Cloudflare WAF blocks #6

Open
opened 2026-06-20 09:41:44 +00:00 by mathias · 1 comment
Owner

Problem

MCP tool calls to the claude.ai Gitea (and by extension Brain) connectors intermittently fail with a Cloudflare "Sorry, you have been blocked" page. The block is on the Anthropic-hosted MCP relay POST -- the error page names anthropic.com (e.g. IP 5.150.212.35), not git.d-ma.be. It triggers on large or command-shaped bodies (WAF: "a SQL command or malformed data"). Observed 2026-06-20 while filing infra#129 (a multi-code-block issue): issue_create over MCP returned the CF block page; the identical issue filed cleanly via the Gitea REST API.

The WAF sits upstream of our gitea-mcp server, in front of Anthropic's connector gateway -- we cannot tune it. The only durable lever is to route around the relay.

Decision (see brain: homelab/decisions/prefer-rest-api-skills-over-mcp-on-cli-cloudflare-waf)

  • Claude Code CLI (koala/iguana/flamingo): default to direct REST-API skills for Gitea + Brain writes / large / command-shaped payloads. koala -> git.d-ma.be / brain-mcp.d-ma.be directly, never through the Anthropic relay -> sidesteps the WAF class entirely.
  • MCP connectors: keep as fallback AND as the only option on claude.ai Desktop/web (no shell / no ~/.op-env there). This is NOT a removal of the MCP.

Deliverables

1. gitea-rest skill

Wrap the common Gitea ops as REST calls via op run --env-file ~/.op-env:

  • issue: create / comment / get / list / edit / close
  • file: read / write-branch / delete
  • pr: create / list / get / merge
  • repo: get / list / status

Pattern (token stays in the subprocess; body from file, never argv):

op run --env-file ~/.op-env -- python3 - <<'PY'
import os, json, urllib.request
body = open('/tmp/body.md').read()
req = urllib.request.Request(
    'https://git.d-ma.be/api/v1/repos/OWNER/REPO/issues',
    data=json.dumps({'title': T, 'body': body}).encode(),
    headers={'Authorization': 'token '+os.environ['DMABE_GITEA_API_TOKEN'],
             'Content-Type': 'application/json'}, method='POST')
print(json.load(urllib.request.urlopen(req, timeout=30))['html_url'])
PY

2. brain-rest skill

  • query: POST https://brain-mcp.d-ma.be/query {"query": "..."} with $BRAIN_MCP_TOKEN
  • write: POST .../write {"content": "...", "filename": "...", "wing": "...", "hall": "..."}
  • answer: POST .../answer (if exposed)

Acceptance criteria

  • gitea-rest SKILL.md installed in ~/dev/.skills/ + mathias/skills, covering the issue/file/pr/repo ops above.
  • brain-rest SKILL.md covering query/write (+ answer).
  • Both wrap all secret use in op run --env-file ~/.op-env; tokens never echoed or placed in argv; bodies passed via file/stdin.
  • Skill trigger guidance: on Claude Code CLI prefer these over the MCP for writes / large or command-shaped payloads; fall back to MCP if ~/.op-env is absent.
  • SKILLS_INDEX.md updated.
  • Smoke test each: create a throwaway issue + a brain query, confirm no relay/WAF in the path.

Constraints / footguns

  • Requires ~/.op-env; non-interactive Bash lacks on-demand 1P (gitea-mcp#36) -> always op run --env-file ~/.op-env.
  • Accept skill+MCP duplication: the relay WAF is upstream and unfixable from our side.
  • Gitea REST per-repo paths work directly (unlike gitea-mcp per-repo tools that 404 to /api/swagger -- see brain reference_gitea_api_fallback).

Refs: gitea-mcp#36, infra#129, agentsquad#31. Brain: homelab/decisions/prefer-rest-api-skills-over-mcp-on-cli-cloudflare-waf.

## Problem MCP tool calls to the claude.ai **Gitea** (and by extension **Brain**) connectors intermittently fail with a Cloudflare "Sorry, you have been blocked" page. The block is on the **Anthropic-hosted MCP relay POST** -- the error page names `anthropic.com` (e.g. IP 5.150.212.35), not `git.d-ma.be`. It triggers on large or command-shaped bodies (WAF: "a SQL command or malformed data"). Observed 2026-06-20 while filing infra#129 (a multi-code-block issue): `issue_create` over MCP returned the CF block page; the identical issue filed cleanly via the Gitea REST API. The WAF sits **upstream of our `gitea-mcp` server**, in front of Anthropic's connector gateway -- we cannot tune it. The only durable lever is to route around the relay. ## Decision (see brain: homelab/decisions/prefer-rest-api-skills-over-mcp-on-cli-cloudflare-waf) - **Claude Code CLI (koala/iguana/flamingo):** default to direct REST-API skills for Gitea + Brain writes / large / command-shaped payloads. koala -> `git.d-ma.be` / `brain-mcp.d-ma.be` directly, never through the Anthropic relay -> sidesteps the WAF class entirely. - **MCP connectors:** keep as fallback AND as the only option on claude.ai Desktop/web (no shell / no `~/.op-env` there). This is NOT a removal of the MCP. ## Deliverables ### 1. `gitea-rest` skill Wrap the common Gitea ops as REST calls via `op run --env-file ~/.op-env`: - issue: create / comment / get / list / edit / close - file: read / write-branch / delete - pr: create / list / get / merge - repo: get / list / status Pattern (token stays in the subprocess; body from file, never argv): ```bash op run --env-file ~/.op-env -- python3 - <<'PY' import os, json, urllib.request body = open('/tmp/body.md').read() req = urllib.request.Request( 'https://git.d-ma.be/api/v1/repos/OWNER/REPO/issues', data=json.dumps({'title': T, 'body': body}).encode(), headers={'Authorization': 'token '+os.environ['DMABE_GITEA_API_TOKEN'], 'Content-Type': 'application/json'}, method='POST') print(json.load(urllib.request.urlopen(req, timeout=30))['html_url']) PY ``` ### 2. `brain-rest` skill - `query`: `POST https://brain-mcp.d-ma.be/query` `{"query": "..."}` with `$BRAIN_MCP_TOKEN` - `write`: `POST .../write` `{"content": "...", "filename": "...", "wing": "...", "hall": "..."}` - `answer`: `POST .../answer` (if exposed) ## Acceptance criteria - [ ] `gitea-rest` SKILL.md installed in `~/dev/.skills/` + `mathias/skills`, covering the issue/file/pr/repo ops above. - [ ] `brain-rest` SKILL.md covering query/write (+ answer). - [ ] Both wrap all secret use in `op run --env-file ~/.op-env`; tokens never echoed or placed in argv; bodies passed via file/stdin. - [ ] Skill trigger guidance: on Claude Code CLI prefer these over the MCP for writes / large or command-shaped payloads; fall back to MCP if `~/.op-env` is absent. - [ ] SKILLS_INDEX.md updated. - [ ] Smoke test each: create a throwaway issue + a brain query, confirm no relay/WAF in the path. ## Constraints / footguns - Requires `~/.op-env`; non-interactive Bash lacks on-demand 1P (gitea-mcp#36) -> always `op run --env-file ~/.op-env`. - Accept skill+MCP duplication: the relay WAF is upstream and unfixable from our side. - Gitea REST per-repo paths work directly (unlike gitea-mcp per-repo tools that 404 to /api/swagger -- see brain reference_gitea_api_fallback). _Refs: gitea-mcp#36, infra#129, agentsquad#31. Brain: homelab/decisions/prefer-rest-api-skills-over-mcp-on-cli-cloudflare-waf._
mathias added the risk/highsize/mtype/feat labels 2026-08-27 05:25:45 +00:00
Author
Owner

Premise check before building anything, per this issue's own risk label.

The evidence against the premise still holding

This session (2026-08-29) has pushed roughly forty writes through the claude.ai Gitea MCP connector — the exact relay path this issue routes around. Many were multi-kilobyte issue comments containing fenced code blocks, shell pipelines, JSON payloads, grep -E regexes, sed expressions and quoted error strings. Several are longer than the infra#129 body that originally triggered the block.

Zero Cloudflare blocks. Not one retry, not one HTML error page.

Command-shaped content specifically — the WAF's stated trigger ("a SQL command or malformed data") — went through repeatedly: op run --env-file=<(...) invocations, git push origin HEAD:main "$TAG", sed -n 's/.*"version"...', kubectl get externalsecret -o jsonpath=.... All accepted.

What that does and does not prove

It does not prove the WAF rule was removed — I cannot see Cloudflare's config, and a rule that fires on a narrower pattern than I happened to send would look identical to no rule at all. What it does establish is that the operational problem this issue describes is not currently reproducing at the volume and shape that produced it.

The recommendation, which is yours to take or leave

Do not build gitea-rest and brain-rest yet. Two new skills exist to be loaded and followed; if the failure they route around does not occur, they are two skills competing for selection against the ones that should fire, plus a second code path for every Gitea operation, plus their own maintenance. #29 exists precisely because unfireable skills degrade selection for the rest.

Three options as I see them:

  1. Close as superseded, with the brain decision note updated to say the WAF blocks stopped reproducing on 2026-08-29 and under what volume. If it returns, the issue is one revert away and the evidence is recorded.
  2. Reduce scope to a smoke test — a small script that exercises the relay with a deliberately command-shaped payload, run on a schedule, so the next occurrence is detected rather than stumbled into. This is the "small tool over prose" answer from #19, and it is maybe thirty lines.
  3. Build both skills as designed. Defensible if you have hit the block recently on a host I have not seen, which I would not know about.

I lean 2: it costs a fraction of 1, and it converts "we think this stopped" into something that will tell us if it starts again.

Worth noting independently of the verdict: the direct-REST pattern this issue specifies is in use and does work. Fetching Gitea Actions job logs today needed it, because no MCP tool exposes that endpoint — op run --env-file ~/.op-env -- curl ... /actions/jobs/{id}/logs. So the technique is proven; the question is only whether it deserves to be a loaded skill or stays an occasional inline call.

Premise check before building anything, per this issue's own risk label. ## The evidence against the premise still holding This session (2026-08-29) has pushed roughly **forty** writes through the claude.ai Gitea MCP connector — the exact relay path this issue routes around. Many were multi-kilobyte issue comments containing fenced code blocks, shell pipelines, JSON payloads, `grep -E` regexes, `sed` expressions and quoted error strings. Several are longer than the infra#129 body that originally triggered the block. **Zero Cloudflare blocks.** Not one retry, not one HTML error page. Command-shaped content specifically — the WAF's stated trigger ("a SQL command or malformed data") — went through repeatedly: `op run --env-file=<(...)` invocations, `git push origin HEAD:main "$TAG"`, `sed -n 's/.*"version"...'`, `kubectl get externalsecret -o jsonpath=...`. All accepted. ## What that does and does not prove It does not prove the WAF rule was removed — I cannot see Cloudflare's config, and a rule that fires on a narrower pattern than I happened to send would look identical to no rule at all. What it does establish is that the *operational* problem this issue describes is not currently reproducing at the volume and shape that produced it. ## The recommendation, which is yours to take or leave **Do not build `gitea-rest` and `brain-rest` yet.** Two new skills exist to be loaded and followed; if the failure they route around does not occur, they are two skills competing for selection against the ones that should fire, plus a second code path for every Gitea operation, plus their own maintenance. #29 exists precisely because unfireable skills degrade selection for the rest. Three options as I see them: 1. **Close as superseded**, with the brain decision note updated to say the WAF blocks stopped reproducing on 2026-08-29 and under what volume. If it returns, the issue is one revert away and the evidence is recorded. 2. **Reduce scope to a smoke test** — a small script that exercises the relay with a deliberately command-shaped payload, run on a schedule, so the next occurrence is detected rather than stumbled into. This is the "small tool over prose" answer from #19, and it is maybe thirty lines. 3. **Build both skills as designed.** Defensible if you have hit the block recently on a host I have not seen, which I would not know about. I lean 2: it costs a fraction of 1, and it converts "we think this stopped" into something that will tell us if it starts again. Worth noting independently of the verdict: the direct-REST pattern this issue specifies **is** in use and does work. Fetching Gitea Actions job logs today needed it, because no MCP tool exposes that endpoint — `op run --env-file ~/.op-env -- curl ... /actions/jobs/{id}/logs`. So the technique is proven; the question is only whether it deserves to be a loaded skill or stays an occasional inline call.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/skills#6