feat!: full-access Gitea MCP server (1.0.0): full API coverage, stdio + streamable HTTP, admin and per-user OAuth #71
No files matched your search
@@ -19,7 +19,11 @@ Full access by default (ADR 0001). The server does not decide what is safe; the
|
|||||||
owner restricts exposure with `READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS` and the
|
owner restricts exposure with `READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS` and the
|
||||||
scope of the Gitea token. Everything else below is load-bearing:
|
scope of the Gitea token. Everything else below is load-bearing:
|
||||||
|
|
||||||
- Every tool call produces one audit event in the hash-chained, append-only log.
|
- Every tool call is audited in the hash-chained, append-only log with a write-ahead
|
||||||
|
model: a `tool_call_started` entry is written before the call executes (the call is
|
||||||
|
refused with `audit_unavailable` when that entry cannot be written), and a
|
||||||
|
`tool_invocation` entry with the outcome is written after it. Anything that counts
|
||||||
|
calls in the log counts `tool_invocation` entries.
|
||||||
- No secrets or tokens in logs, errors, audit entries or results. Output is
|
- No secrets or tokens in logs, errors, audit entries or results. Output is
|
||||||
passed through secret masking (`SECRET_DETECTION_MODE`, default `mask`).
|
passed through secret masking (`SECRET_DETECTION_MODE`, default `mask`).
|
||||||
`Identity.token` and every `SecretStr` setting never appear in `repr`.
|
`Identity.token` and every `SecretStr` setting never appear in `repr`.
|
||||||
@@ -62,18 +66,21 @@ src/aegis_gitea_mcp/
|
|||||||
errors.py ToolError, GiteaAPIError(status, kind, message)
|
errors.py ToolError, GiteaAPIError(status, kind, message)
|
||||||
audit.py hash-chained audit log; security.py secret masking;
|
audit.py hash-chained audit log; security.py secret masking;
|
||||||
response_limits.py, rate_limit.py (HTTP), request_context.py, logging_utils.py
|
response_limits.py, rate_limit.py (HTTP), request_context.py, logging_utils.py
|
||||||
server.py, transports, oauth/ planned (chunks B4, B5), not present yet
|
server.py the low-level SDK Server: tool list, call dispatcher, audit calls
|
||||||
scripts/ generate_catalog.py, validate_audit_log.py, ci/, check_docs.py (B11)
|
transports.py stdio runner and the streamable HTTP app
|
||||||
|
auth.py static bearer verification and identity resolution
|
||||||
|
oauth/ per-user OAuth: provider, resolver, encrypted store, upstream, callback
|
||||||
|
scripts/ generate_catalog.py, validate_audit_log.py, check_docs.py, ci/
|
||||||
spec/ pinned Gitea swagger
|
spec/ pinned Gitea swagger
|
||||||
tests/integration/ live Gitea in Docker (harness.py), opt-in
|
tests/integration/ live Gitea in Docker (harness.py), opt-in; sweep of every tool
|
||||||
|
tests/mcp/ MCP smoke tests (stdio, HTTP, container image), opt-in
|
||||||
|
tests/security/ security regression suite
|
||||||
```
|
```
|
||||||
|
|
||||||
Modules marked planned do not exist yet. Request flow (target):
|
Request flow: client -> SDK transport and auth verifier -> `Identity` ->
|
||||||
client -> SDK transport and auth verifier -> `Identity` -> toolset/read-only
|
toolset/read-only filters -> schema validation -> `tool_call_started` audit entry
|
||||||
filters -> schema validation -> executor (Gitea call with the identity's token,
|
-> executor (Gitea call with the identity's token, optional `sudo`) -> limits and
|
||||||
optional `sudo`) -> limits and masking -> result envelope (ADR 0005) -> audit.
|
masking -> result envelope (ADR 0005) -> `tool_invocation` audit entry.
|
||||||
The executor and the catalog exist; the server and transports that connect them
|
|
||||||
to MCP clients do not yet.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -87,13 +94,14 @@ make lint | format | test | test-integration | run | validate-audit | docker-bui
|
|||||||
```
|
```
|
||||||
|
|
||||||
Live-Gitea integration tests carry the `integration` marker and run only with
|
Live-Gitea integration tests carry the `integration` marker and run only with
|
||||||
`AEGIS_INTEGRATION=1` (`make test-integration`).
|
`AEGIS_INTEGRATION=1` (`make test-integration`); the MCP smoke tests under
|
||||||
|
`tests/mcp` carry `mcp_smoke`. `tests/integration/sweep_baseline.json` pins the
|
||||||
|
expected outcome of every tool, see `tests/integration/README.md`.
|
||||||
|
|
||||||
## Quality gates
|
## Quality gates
|
||||||
|
|
||||||
Ruff (E,W,F,I,B,C4,UP,S,SIM,RUF), ruff format, mypy and pytest must be green
|
Ruff (E,W,F,I,B,C4,UP,S,SIM,RUF), ruff format, mypy and pytest must be green
|
||||||
before every commit. Coverage threshold is 80% until the final chunk and 90%
|
before every commit. The coverage threshold is 90%; never lower it. Tests must not write outside `tmp_path`
|
||||||
from then on; never lower it. Tests must not write outside `tmp_path`
|
|
||||||
(the autouse fixture in `tests/conftest.py` isolates env, cwd, `DATA_DIR`,
|
(the autouse fixture in `tests/conftest.py` isolates env, cwd, `DATA_DIR`,
|
||||||
`AUDIT_LOG_PATH` and singletons). Python 3.10+, line length 100, public
|
`AUDIT_LOG_PATH` and singletons). Python 3.10+, line length 100, public
|
||||||
functions need docstrings and type hints. Documentation lives under `docs/`;
|
functions need docstrings and type hints. Documentation lives under `docs/`;
|
||||||
|
|||||||
@@ -12,12 +12,9 @@ HTTP, using the official `mcp` SDK v2 low-level `Server`. Full access by default
|
|||||||
and the Gitea token scope. Auth modes: admin-token (`GITEA_TOKEN` / static
|
and the Gitea token scope. Auth modes: admin-token (`GITEA_TOKEN` / static
|
||||||
`MCP_BEARER_TOKEN`) and per-user OAuth on HTTP (user's own Gitea token).
|
`MCP_BEARER_TOKEN`) and per-user OAuth on HTTP (user's own Gitea token).
|
||||||
|
|
||||||
The project is mid-rebuild (chunk plan in `docs/audit.md`). Built so far: tooling,
|
The rebuild is complete: catalog, executor, server, both transports, per-user OAuth,
|
||||||
`config.py`, `identity.py`, `errors.py`, the generated catalog (`catalog/`), the
|
setup command, CI, security suite, MCP smoke tests and the live integration suite are in.
|
||||||
Gitea executor (`gitea/`), the setup command (`setup_cmd.py`, `setup_snippets.py`,
|
The remaining open items are tracked in `docs/roadmap.md`.
|
||||||
`setup_template.py`), the integration harness, CI and audit/security/limits
|
|
||||||
helpers. Planned: server and transports (B4), OAuth (B5), smoke tests (B9),
|
|
||||||
docs (B11).
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -27,7 +24,7 @@ make lint # ruff check, ruff format --check, mypy src
|
|||||||
make format # ruff format + ruff check --fix
|
make format # ruff format + ruff check --fix
|
||||||
make test # uv run pytest (coverage gate)
|
make test # uv run pytest (coverage gate)
|
||||||
make test-integration # AEGIS_INTEGRATION=1, needs Docker
|
make test-integration # AEGIS_INTEGRATION=1, needs Docker
|
||||||
make run # uv run aegis-gitea-mcp serve (not implemented until B4)
|
make run # uv run aegis-gitea-mcp serve
|
||||||
uv run python scripts/generate_catalog.py --check # catalog matches the spec
|
uv run python scripts/generate_catalog.py --check # catalog matches the spec
|
||||||
make validate-audit # verify audit hash chain
|
make validate-audit # verify audit hash chain
|
||||||
uv run pytest tests/test_config.py::test_defaults -v # single test
|
uv run pytest tests/test_config.py::test_defaults -v # single test
|
||||||
@@ -44,11 +41,12 @@ uv run pytest tests/test_config.py::test_defaults -v # single test
|
|||||||
upload building, `SLASH_ROUTES`), `pagination.py`, `redaction.py`.
|
upload building, `SLASH_ROUTES`), `pagination.py`, `redaction.py`.
|
||||||
- `setup_cmd.py`, `setup_snippets.py`, `setup_template.py` (done): the `setup`
|
- `setup_cmd.py`, `setup_snippets.py`, `setup_template.py` (done): the `setup`
|
||||||
command, its client snippets and the built-in `.env` template.
|
command, its client snippets and the built-in `.env` template.
|
||||||
- Server (B4, planned): low-level SDK `Server` with one `on_list_tools` and one
|
- `server.py` (done): low-level SDK `Server` with one `on_list_tools` and one
|
||||||
`on_call_tool` dispatcher, jsonschema validation, audit, `is_error` results for
|
`on_call_tool` dispatcher, jsonschema validation, audit, `is_error` results for
|
||||||
tool failures, `gitea_api_request` escape-hatch tool.
|
tool failures, `gitea_api_request` escape-hatch tool. `transports.py`: stdio
|
||||||
- Auth (B5, planned): static bearer verifier and OAuth provider with encrypted
|
runner and streamable HTTP app.
|
||||||
token store.
|
- `auth.py` and `oauth/` (done): static bearer verifier and the OAuth provider
|
||||||
|
with encrypted token store.
|
||||||
- Core helpers (done): `config.py` (`Settings`, `get_settings()`,
|
- Core helpers (done): `config.py` (`Settings`, `get_settings()`,
|
||||||
`reset_settings()`), `identity.py`, `errors.py` (`ToolError`,
|
`reset_settings()`), `identity.py`, `errors.py` (`ToolError`,
|
||||||
`GiteaAPIError`), `audit.py`, `security.py`, `response_limits.py`,
|
`GiteaAPIError`), `audit.py`, `security.py`, `response_limits.py`,
|
||||||
@@ -61,7 +59,9 @@ points `DATA_DIR`/`AUDIT_LOG_PATH` into `tmp_path`.
|
|||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
Kept: audit event per tool call; no secrets in logs/errors/results; no stack
|
Kept: write-ahead audit (a `tool_call_started` entry before the call runs, the
|
||||||
|
call is refused when it cannot be written, a `tool_invocation` entry with the
|
||||||
|
outcome after it; count calls by `tool_invocation`); no secrets in logs/errors/results; no stack
|
||||||
traces by default; `127.0.0.1` default bind and fail-closed HTTP auth off
|
traces by default; `127.0.0.1` default bind and fail-closed HTTP auth off
|
||||||
loopback; untrusted-content labelling; schema validation with no extra
|
loopback; untrusted-content labelling; schema validation with no extra
|
||||||
properties; response size bounds; validated `GITEA_URL` and strict
|
properties; response size bounds; validated `GITEA_URL` and strict
|
||||||
@@ -73,7 +73,7 @@ Details in `AGENTS.md`.
|
|||||||
|
|
||||||
Python 3.10+, line length 100, ruff (E,W,F,I,B,C4,UP,S,SIM,RUF) plus ruff format,
|
Python 3.10+, line length 100, ruff (E,W,F,I,B,C4,UP,S,SIM,RUF) plus ruff format,
|
||||||
strict-ish mypy (`disallow_untyped_defs`, relaxed in tests). Public functions need
|
strict-ish mypy (`disallow_untyped_defs`, relaxed in tests). Public functions need
|
||||||
docstrings and type hints. Coverage 80% now, 90% from the final chunk. All
|
docstrings and type hints. Coverage gate 90%. All
|
||||||
documentation goes under `docs/`; security-impacting changes update docs in the
|
documentation goes under `docs/`; security-impacting changes update docs in the
|
||||||
same changeset. No em-dashes in text.
|
same changeset. No em-dashes in text.
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ GiteaExecutor (gitea/executor.py)
|
|||||||
Gitea REST API (/api/v1)
|
Gitea REST API (/api/v1)
|
||||||
```
|
```
|
||||||
|
|
||||||
Every tool call ends in one audit event, and every result is wrapped in the same envelope.
|
Every tool call is audited (a write-ahead `tool_call_started` entry, then one `tool_invocation` entry with the outcome), and every result is wrapped in the same envelope.
|
||||||
|
|
||||||
## Catalog
|
## Catalog
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -26,12 +26,12 @@ because credentials belong in the identity, not in the URL.
|
|||||||
## Behaviour
|
## Behaviour
|
||||||
|
|
||||||
- It goes through the same executor as every generated tool: the identity's token, no redirects,
|
- It goes through the same executor as every generated tool: the identity's token, no redirects,
|
||||||
response limits, redaction and secret masking, the result envelope and one audit event.
|
response limits, redaction and secret masking, the result envelope and the same audit entries (`tool_call_started`, then `tool_invocation`).
|
||||||
- There is no allow list of paths and no write classification: with full access, the token's
|
- There is no allow list of paths and no write classification: with full access, the token's
|
||||||
scopes and the identity's Gitea permissions are the limit.
|
scopes and the identity's Gitea permissions are the limit.
|
||||||
- `READ_ONLY=true` keeps the tool listed but it accepts only `GET`.
|
- `READ_ONLY=true` keeps the tool listed but it accepts only `GET`.
|
||||||
- `TOOLSETS` without `misc`, or `DISABLED_TOOLS=gitea_api_request`, removes it.
|
- `TOOLSETS` without `misc`, or `DISABLED_TOOLS=gitea_api_request`, removes it.
|
||||||
- The audit event records the method and the path, so it is visible which endpoint was called.
|
- The audit entry records the method and the path, so it is visible which endpoint was called.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
Start with the result of the failing call: errors carry `error.status`, `error.kind` and
|
Start with the result of the failing call: errors carry `error.status`, `error.kind` and
|
||||||
Gitea's sanitized message, and the audit log has one event per call (see
|
Gitea's sanitized message, and the audit log has a result entry (`tool_invocation`) per call, preceded by a `tool_call_started` entry (see
|
||||||
[audit-logging.md](audit-logging.md)).
|
[audit-logging.md](audit-logging.md)).
|
||||||
|
|
||||||
## The server does not start
|
## The server does not start
|
||||||
|
|||||||
Reference in new issue
Block a user