feat!: full-access Gitea MCP server (1.0.0): full API coverage, stdio + streamable HTTP, admin and per-user OAuth #71

Merged
Latte merged 70 commits from feat/full-api-coverage into dev 2026-10-14 17:25:25 +00:00
Showing only changes of commit 10c8b21cf5 - Show all commits
@@ -27,3 +27,72 @@ Every successful tool call returns one text content item with JSON:
Messages are truncated and stripped of tokens and the Authorization header.
- Argument validation failures become `is_error=True` with the jsonschema path.
- Unknown tool names raise `MCPError(INVALID_PARAMS)`.
## Amendments
### Secret handling in results (implemented in `gitea/executor.py`, `gitea/encoding.py`)
Two layers run on every result, success and error, before the size limit:
1. Exact redaction (always on, independent of `SECRET_DETECTION_MODE`). The
following values are replaced with `[REDACTED]` wherever they occur in a
string or dict key of the payload: the token of the calling identity,
`GITEA_TOKEN`, `MCP_BEARER_TOKEN`, `GITEA_OAUTH_CLIENT_SECRET`, the
`extra_secrets` given to the executor, and secret values submitted in the
request body of the same call.
2. Pattern masking (`SECRET_DETECTION_MODE`). `off` skips it. `mask` replaces
each match of the secret patterns (provider keys, JWTs, private key headers,
generic `api_key`/`token` assignments) with a shortened form. `block`
withholds the whole result and returns an `is_error` result of kind
`secret_blocked`.
Request-body secret fields. Values of these fields, when at least 4 characters
long, are collected from the submitted body (nested objects and lists up to a
depth of 8) and redacted from the result of that call, because Gitea echoes
some of them back (a created webhook returns its `authorization_header`):
`password`, `secret`, `client_secret`, `token`, `access_token`, `auth_password`,
`auth_token`, `private_key`, `authorization_header`, `remote_password`,
`mirror_password`, `mirror_token`, `aws_secret_access_key`, `aws_access_key_id`.
For Action secret writes the `data` value is redacted at any length. Only the
value sent in the same call is known to the executor, so a later read of an
object that stores such a value is not covered by this rule (pattern masking
still applies).
Credential-issuing operations. The successful results of
`CREDENTIAL_ISSUING_OPERATIONS` skip pattern masking and block mode, because
their whole purpose is to hand the caller a freshly minted credential that
masking would make unusable (and block mode would refuse to deliver):
- `userCreateToken` (access token)
- `adminCreateRunnerRegistrationToken`, `orgCreateRunnerRegistrationToken`,
`repoCreateRunnerRegistrationToken`, `userCreateRunnerRegistrationToken`
(Actions runner registration tokens, delivered in a response header)
- `userCreateOAuth2Application`, `userUpdateOAuth2Application` (OAuth2
`client_secret`)
- `repoCreateDeployToken` (deploy token)
Exact redaction still applies to these results, so the caller's own tokens are
never echoed, and their errors are masked like every other error. Operators who
do not want any credential to pass through the model context should disable
these tools with `DISABLED_TOOLS`.
Hardening of the result path: JSON from Gitea nested deeper than 100 levels is
returned as text instead of being parsed (the recursive redaction and masking
passes must not hit the interpreter recursion limit), and a base64 file upload
whose decoded size would exceed `MAX_BINARY_BYTES` is rejected before it is
decoded with kind `payload_too_large`.
### Path encoding and the raw tool
Path values are percent-encoded with `/` escaped as `%2F`, except for the
parameters that Gitea 28.1.0 serves through wildcard routes (branch, tag, file
path, ref and compare names on `branches`, `tags`, `contents`, `contents-ext`,
`raw`, `media`, `compare`, `git/refs`, `commits/{ref}/status(es)` and
`pulls/{base}/{head}`). The decision is keyed on the route and the parameter,
not the parameter name: `releases/tags/{tag}` and `editorconfig/{filepath}` are
single-segment routes that only resolve with `%2F`.
The raw tool `gitea_api_request` rejects `%` in paths, so it cannot send
`%2F`. A route with a single-segment parameter whose value contains a slash
(a release tag such as `rel/1.0`) must be called through the generated tool for
that operation.