Audit of both Tea MCPs as of 2026-10-15, with a plan for one smaller one. The operator asked for it, expecting a refactor or golf.
What exists
old Tea MCP (the shell)
Teahub MCP (prod)
source
matcha/teahubtea_mcp/ on hb-5
teahubxyz/teahubmcp/tea_mcp/server.py
size
~8,500 lines, 24 modules, ~50 tools, 50 test files
408 lines, 1 file, 17 tools, no tests
runs as
container teahub-mcp on hb-5 :8090, plus the phast sidecar on :8091
Fly green-tea-mcp, mcp.teahub.xyz
authority
mixed: caller tokens, plus a factory service token (oracle), an operator/member gate, shell and agent lanes
always the caller's own Gitea token, no service token, nothing deletes
status
dead.GITEA_URL=https://matcha.teahub.xyz no longer resolves, PUBLIC_URL=https://mcp.teahub.xyz is prod's now, and nothing public reaches it. The only remaining callers are Boring's boring_teahub_* tools.
live. hb-5's Codex config points at it, so Codex needs to re-authorize against the new OAuth.
tea_repo_commit: several files in one commit, including binary files such as screenshots.
tea_action_jobs / tea_action_logs: job logs. Today they were read through raw admin API calls.
chat_tools uploads: binary files.
Drop, now done elsewhere:
phast sidecar → phast-app
persimmons / human vault / juice → persimmons/app (persimmons/app#1)
breakglass, domains → Boring
pages → green-tea-pages
factory milestones and children → Time
Drop, contradicts prod's model:
oracle: a factory service-token passthrough
shell_exec, agent_*, queue, usage: the old Codex worker. Seats now run in Time's Actions.
tea_boring_*: the reciprocal lane. Boring deploys through its own workflow in the shell now.
Drop, experiments:tea_status_app, signal_dive_app, the avatar and voice modules. They can stay in the shell's history.
Prod MCP: findings
Validation boilerplate is repeated in every tool (number >= 1, page/limit ranges, state in {...}, name regexes): about 60 lines. Annotated[int, Field(ge=1, le=50)], Literal[...] and a RepoName type would let FastMCP validate it and publish the limits in the JSON schema, so clients see them.
tea_issue_create drops unknown label names silently, and only reads the first 50 labels. It should refuse unknown labels, or say which ones it skipped.
tea_file_change decides create vs update by matching "404" in an error string. One get_or_none() helper would fix this, and tea_file_get/tea_tree could use it too.
tea_org_add_member defaults to the Owners team. That's a sharp default for a one-call tool; the team should be required.
Inconsistent defaults:tea_repo_create makes private repos, tea_org_repo_create makes public ones.
Silent truncation:tea_whoami lists only 50 repos, tea_tree caps at 500 entries. Both should say when they truncate.
The CIMD monkeypatch (fastmcp_cimd.ssrf_safe_fetch_response) and the hardcoded Claude client fallback are global patches that a FastMCP upgrade can break. They need a comment explaining why they exist, and a check whether 4.0.10 still needs them.
No tests. One fake-Gitea test file (Time already has the pattern) would cover the helpers and the tools.
Missing tools forced temporary admin tokens. Six tokens were minted on the Fly box on 2026-10-14 (ids 35–40), plus 42 on 2026-10-15, all deleted. They covered archiving repos, webhooks, multi-file and binary pushes, and job logs. As caller-authority tools, these would work for anyone Gitea already allows, with no token minting.
Plan (one file, about 450 lines, about 27 tools)
Typed parameters with Annotated/Literal, one get_or_none, a truthful truncated flag. This is the golf, about −80 lines.
Add, all as the caller:
pulls: tea_pull_get (with diff and status), tea_pull_create, tea_pull_review, tea_pull_merge
tea_labels and label/unlabel
tea_branch_create
tea_commit (multi-file, base64 for binaries)
tea_action_logs
tea_repo_edit (description, website, archived)
tea_hook_create (push hooks for pages)
tea_secret_set / tea_variable_set
Fix findings 2–7. Add tests/test_mcp.py.
Retire the old container teahub-mcp and its phast sidecar on hb-5. Point Boring's boring_teahub_* at green-tea-mcp (Fly status, logs, restart) or remove them; that ships through deploy-boring-runtime.
Re-authorize Codex on hb-5 against the new mcp.teahub.xyz.
DONE: one file, typed, tested. Today's chores (archive a repo, add a pages hook, commit a screenshot, read a job log, set a secret) work through the MCP as a signed-in owner, with no admin token minted. The old container is stopped.
Audit of both Tea MCPs as of 2026-10-15, with a plan for one smaller one. The operator asked for it, expecting a refactor or golf.
## What exists
| | old Tea MCP (the shell) | Teahub MCP (prod) |
|---|---|---|
| source | `matcha/teahub` `tea_mcp/` on hb-5 | `teahubxyz/teahub` `mcp/tea_mcp/server.py` |
| size | ~8,500 lines, 24 modules, ~50 tools, 50 test files | 408 lines, 1 file, 17 tools, no tests |
| runs as | container `teahub-mcp` on hb-5 :8090, plus the phast sidecar on :8091 | Fly `green-tea-mcp`, mcp.teahub.xyz |
| authority | mixed: caller tokens, plus a factory service token (`oracle`), an operator/member gate, shell and agent lanes | always the caller's own Gitea token, no service token, nothing deletes |
| status | **dead.** `GITEA_URL=https://matcha.teahub.xyz` no longer resolves, `PUBLIC_URL=https://mcp.teahub.xyz` is prod's now, and nothing public reaches it. The only remaining callers are Boring's `boring_teahub_*` tools. | live. hb-5's Codex config points at it, so Codex needs to re-authorize against the new OAuth. |
## The old one: keep, port or drop
- **Already on prod:** repo, issues, comments, file get/change, actions list, workflow dispatch, repo create.
- **Port** (tools that today's work missed):
- `tea_repo_commit`: several files in one commit, including binary files such as screenshots.
- `tea_action_jobs` / `tea_action_logs`: job logs. Today they were read through raw admin API calls.
- `chat_tools` uploads: binary files.
- **Drop, now done elsewhere:**
- phast sidecar → `phast-app`
- persimmons / human vault / juice → `persimmons/app` (persimmons/app#1)
- breakglass, domains → Boring
- pages → `green-tea-pages`
- factory milestones and children → Time
- **Drop, contradicts prod's model:**
- `oracle`: a factory service-token passthrough
- `shell_exec`, `agent_*`, `queue`, `usage`: the old Codex worker. Seats now run in Time's Actions.
- `tea_boring_*`: the reciprocal lane. Boring deploys through its own workflow in the shell now.
- **Drop, experiments:** `tea_status_app`, `signal_dive_app`, the avatar and voice modules. They can stay in the shell's history.
## Prod MCP: findings
1. **Validation boilerplate** is repeated in every tool (`number >= 1`, page/limit ranges, `state in {...}`, name regexes): about 60 lines. `Annotated[int, Field(ge=1, le=50)]`, `Literal[...]` and a `RepoName` type would let FastMCP validate it and publish the limits in the JSON schema, so clients see them.
2. **`tea_issue_create` drops unknown label names silently**, and only reads the first 50 labels. It should refuse unknown labels, or say which ones it skipped.
3. **`tea_file_change` decides create vs update by matching `"404"` in an error string.** One `get_or_none()` helper would fix this, and `tea_file_get`/`tea_tree` could use it too.
4. **`tea_org_add_member` defaults to the Owners team.** That's a sharp default for a one-call tool; the team should be required.
5. **Inconsistent defaults:** `tea_repo_create` makes private repos, `tea_org_repo_create` makes public ones.
6. **Silent truncation:** `tea_whoami` lists only 50 repos, `tea_tree` caps at 500 entries. Both should say when they truncate.
7. **The CIMD monkeypatch** (`fastmcp_cimd.ssrf_safe_fetch_response`) and the hardcoded Claude client fallback are global patches that a FastMCP upgrade can break. They need a comment explaining why they exist, and a check whether 4.0.10 still needs them.
8. **No tests.** One fake-Gitea test file (Time already has the pattern) would cover the helpers and the tools.
9. **Missing tools forced temporary admin tokens.** Six tokens were minted on the Fly box on 2026-10-14 (ids 35–40), plus 42 on 2026-10-15, all deleted. They covered archiving repos, webhooks, multi-file and binary pushes, and job logs. As caller-authority tools, these would work for anyone Gitea already allows, with no token minting.
## Plan (one file, about 450 lines, about 27 tools)
- [ ] Typed parameters with Annotated/Literal, one `get_or_none`, a truthful `truncated` flag. This is the golf, about −80 lines.
- [ ] Add, all as the caller:
- pulls: `tea_pull_get` (with diff and status), `tea_pull_create`, `tea_pull_review`, `tea_pull_merge`
- `tea_labels` and label/unlabel
- `tea_branch_create`
- `tea_commit` (multi-file, base64 for binaries)
- `tea_action_logs`
- `tea_repo_edit` (description, website, archived)
- `tea_hook_create` (push hooks for pages)
- `tea_secret_set` / `tea_variable_set`
- [ ] Fix findings 2–7. Add `tests/test_mcp.py`.
- [ ] Retire the old container `teahub-mcp` and its phast sidecar on hb-5. Point Boring's `boring_teahub_*` at `green-tea-mcp` (Fly status, logs, restart) or remove them; that ships through `deploy-boring-runtime`.
- [ ] Re-authorize Codex on hb-5 against the new mcp.teahub.xyz.
DONE: one file, typed, tested. Today's chores (archive a repo, add a pages hook, commit a screenshot, read a job log, set a secret) work through the MCP as a signed-in owner, with no admin token minted. The old container is stopped.
Done, except for two things this code can't do on its own (job logs and Codex's sign-in, below).
Server:mcp/tea_mcp/server.py (fb8acb0, then fbb1159 = 2.0.1) is one file, 650 lines for 29 tools, all acting as the caller. Parameters are typed, so bad arguments are refused before Gitea is asked. Everything planned is there: pulls get/create/review/merge, tea_labels and tea_label, tea_branch_create, tea_commit (text or base64, up to 50 files), tea_repo_edit (archive included), tea_hook_create, tea_secret_set and tea_variable_set. Findings 1–6 and 8 are fixed; tests are mcp/tests/test_server.py (27, against a fake Gitea). For 7, the two shims now have a comment saying why they exist. I haven't tested whether FastMCP 4.0.10 still needs them.
Checked live: every tool ran once against teahub.xyz in a scratch repo and org, both deleted afterwards. That covered a binary commit, an archive, a hook, a secret, a variable, a squash merge (the head branch was kept) and a dispatched workflow that ran on fly-iad-1. The Claude connector kept working through both deploys with no new sign-in.
Job logs can't work on prod yet. Gitea 1.24.7 has no API that lists a run's jobs. /actions/runs returns 404 there, which means the old tea_actions never worked. tea_actions now reads /actions/tasks and links each run's page. The shell's Gitea (28.0.0) does have the runs/jobs/logs API, so upgrading prod's Gitea would make tea_action_logs possible.
Old container:teahub-mcp on hb-5 (with its Phast sidecar) had been idle since the cutover. It is stopped and set not to restart; the container and its volume are kept.
Boring:boring_teahub_* now operate green-tea-mcp (matcha/boring 122f0b3, shipped by deploy-boring-runtime). boring_teahub_deploy shipped 2.0.1 from the exact main SHA, and boring_teahub_status reads that SHA back from the src-<sha> image tag (fresh: true).
Codex on hb-5 points at mcp.teahub.xyz, but codex mcp list shows tea (and boring) as "Not logged in". Running codex mcp login tea on the Mac signs it in; it needs the operator's own sign-in.
Tokens: 43 and 45 (teahubxyz) were deleted by id. The table now shows only 25 (time), and the API answers 401 to both. The receipt is in docs/decisions.md (a5a6688).
Done, except for two things this code can't do on its own (job logs and Codex's sign-in, below).
- **Server:** `mcp/tea_mcp/server.py` (`fb8acb0`, then `fbb1159` = 2.0.1) is one file, 650 lines for 29 tools, all acting as the caller. Parameters are typed, so bad arguments are refused before Gitea is asked. Everything planned is there: pulls get/create/review/merge, `tea_labels` and `tea_label`, `tea_branch_create`, `tea_commit` (text or base64, up to 50 files), `tea_repo_edit` (archive included), `tea_hook_create`, `tea_secret_set` and `tea_variable_set`. Findings 1–6 and 8 are fixed; tests are `mcp/tests/test_server.py` (27, against a fake Gitea). For 7, the two shims now have a comment saying why they exist. I haven't tested whether FastMCP 4.0.10 still needs them.
- **Checked live:** every tool ran once against teahub.xyz in a scratch repo and org, both deleted afterwards. That covered a binary commit, an archive, a hook, a secret, a variable, a squash merge (the head branch was kept) and a dispatched workflow that ran on `fly-iad-1`. The Claude connector kept working through both deploys with no new sign-in.
- **Job logs can't work on prod yet.** Gitea 1.24.7 has no API that lists a run's jobs. `/actions/runs` returns 404 there, which means the old `tea_actions` never worked. `tea_actions` now reads `/actions/tasks` and links each run's page. The shell's Gitea (28.0.0) does have the runs/jobs/logs API, so upgrading prod's Gitea would make `tea_action_logs` possible.
- **Old container:** `teahub-mcp` on hb-5 (with its Phast sidecar) had been idle since the cutover. It is stopped and set not to restart; the container and its volume are kept.
- **Boring:** `boring_teahub_*` now operate `green-tea-mcp` (matcha/boring `122f0b3`, shipped by `deploy-boring-runtime`). `boring_teahub_deploy` shipped 2.0.1 from the exact main SHA, and `boring_teahub_status` reads that SHA back from the `src-<sha>` image tag (`fresh: true`).
- **Codex on hb-5** points at mcp.teahub.xyz, but `codex mcp list` shows `tea` (and `boring`) as "Not logged in". Running `codex mcp login tea` on the Mac signs it in; it needs the operator's own sign-in.
- **Tokens:** 43 and 45 (`teahubxyz`) were deleted by id. The table now shows only 25 (`time`), and the API answers 401 to both. The receipt is in docs/decisions.md (`a5a6688`).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Audit of both Tea MCPs as of 2026-10-15, with a plan for one smaller one. The operator asked for it, expecting a refactor or golf.
What exists
matcha/teahubtea_mcp/on hb-5teahubxyz/teahubmcp/tea_mcp/server.pyteahub-mcpon hb-5 :8090, plus the phast sidecar on :8091green-tea-mcp, mcp.teahub.xyzoracle), an operator/member gate, shell and agent lanesGITEA_URL=https://matcha.teahub.xyzno longer resolves,PUBLIC_URL=https://mcp.teahub.xyzis prod's now, and nothing public reaches it. The only remaining callers are Boring'sboring_teahub_*tools.The old one: keep, port or drop
tea_repo_commit: several files in one commit, including binary files such as screenshots.tea_action_jobs/tea_action_logs: job logs. Today they were read through raw admin API calls.chat_toolsuploads: binary files.phast-apppersimmons/app(persimmons/app#1)green-tea-pagesoracle: a factory service-token passthroughshell_exec,agent_*,queue,usage: the old Codex worker. Seats now run in Time's Actions.tea_boring_*: the reciprocal lane. Boring deploys through its own workflow in the shell now.tea_status_app,signal_dive_app, the avatar and voice modules. They can stay in the shell's history.Prod MCP: findings
number >= 1, page/limit ranges,state in {...}, name regexes): about 60 lines.Annotated[int, Field(ge=1, le=50)],Literal[...]and aRepoNametype would let FastMCP validate it and publish the limits in the JSON schema, so clients see them.tea_issue_createdrops unknown label names silently, and only reads the first 50 labels. It should refuse unknown labels, or say which ones it skipped.tea_file_changedecides create vs update by matching"404"in an error string. Oneget_or_none()helper would fix this, andtea_file_get/tea_treecould use it too.tea_org_add_memberdefaults to the Owners team. That's a sharp default for a one-call tool; the team should be required.tea_repo_createmakes private repos,tea_org_repo_createmakes public ones.tea_whoamilists only 50 repos,tea_treecaps at 500 entries. Both should say when they truncate.fastmcp_cimd.ssrf_safe_fetch_response) and the hardcoded Claude client fallback are global patches that a FastMCP upgrade can break. They need a comment explaining why they exist, and a check whether 4.0.10 still needs them.Plan (one file, about 450 lines, about 27 tools)
get_or_none, a truthfultruncatedflag. This is the golf, about −80 lines.tea_pull_get(with diff and status),tea_pull_create,tea_pull_review,tea_pull_mergetea_labelsand label/unlabeltea_branch_createtea_commit(multi-file, base64 for binaries)tea_action_logstea_repo_edit(description, website, archived)tea_hook_create(push hooks for pages)tea_secret_set/tea_variable_settests/test_mcp.py.teahub-mcpand its phast sidecar on hb-5. Point Boring'sboring_teahub_*atgreen-tea-mcp(Fly status, logs, restart) or remove them; that ships throughdeploy-boring-runtime.DONE: one file, typed, tested. Today's chores (archive a repo, add a pages hook, commit a screenshot, read a job log, set a secret) work through the MCP as a signed-in owner, with no admin token minted. The old container is stopped.
Done, except for two things this code can't do on its own (job logs and Codex's sign-in, below).
mcp/tea_mcp/server.py(fb8acb0, thenfbb1159= 2.0.1) is one file, 650 lines for 29 tools, all acting as the caller. Parameters are typed, so bad arguments are refused before Gitea is asked. Everything planned is there: pulls get/create/review/merge,tea_labelsandtea_label,tea_branch_create,tea_commit(text or base64, up to 50 files),tea_repo_edit(archive included),tea_hook_create,tea_secret_setandtea_variable_set. Findings 1–6 and 8 are fixed; tests aremcp/tests/test_server.py(27, against a fake Gitea). For 7, the two shims now have a comment saying why they exist. I haven't tested whether FastMCP 4.0.10 still needs them.fly-iad-1. The Claude connector kept working through both deploys with no new sign-in./actions/runsreturns 404 there, which means the oldtea_actionsnever worked.tea_actionsnow reads/actions/tasksand links each run's page. The shell's Gitea (28.0.0) does have the runs/jobs/logs API, so upgrading prod's Gitea would maketea_action_logspossible.teahub-mcpon hb-5 (with its Phast sidecar) had been idle since the cutover. It is stopped and set not to restart; the container and its volume are kept.boring_teahub_*now operategreen-tea-mcp(matcha/boring122f0b3, shipped bydeploy-boring-runtime).boring_teahub_deployshipped 2.0.1 from the exact main SHA, andboring_teahub_statusreads that SHA back from thesrc-<sha>image tag (fresh: true).codex mcp listshowstea(andboring) as "Not logged in". Runningcodex mcp login teaon the Mac signs it in; it needs the operator's own sign-in.teahubxyz) were deleted by id. The table now shows only 25 (time), and the API answers 401 to both. The receipt is in docs/decisions.md (a5a6688).