get_issue raised 'NoneType' object is not iterable on issues whose
labels/assignees Gitea returns as null or with non-dict elements (the #13
class), which reached clients as an opaque JSON-RPC -32603 with no detail.
- read_tools: skip non-dict label/assignee entries in get_issue_tool
- server: detect a wrapped GiteaNotFoundError via the __cause__ chain and
return 404 / JSON-RPC -32000 with a clear message; include the exception
type name in masked internal errors so future masked failures are
diagnosable without exposing messages or stack traces
- tests: cover non-dict collection elements and the not-found / typed-error
responses
- ci: rewrite docker.yml to build, smoke-test and push the image to the
Gitea container registry on merge to main/dev, matching the hiddenden.cafe
pattern (only REGISTRY_TOKEN required)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds reusable, secret-safe logging helpers to `logging_utils`:
- `log_event(logger, level, event, **context)` emits a named event with a
sanitized `context` mapping (sensitive keys masked as `***`).
- `log_nullable_field(...)` records whether a parsed field is None plus its
runtime type, without dumping its contents.
- `sanitize_context(...)` is the shared masking primitive.
The JSON formatter now serializes a record's `context` into the payload.
`get_issue_tool` is instrumented at DEBUG (`get_issue.start`,
`get_issue.payload_shape`, `get_issue.field_check` for labels/assignees/user)
so the nullable-field parsing that caused #13 is diagnosable going forward.
Adds tests for the helpers, the formatter, and the get_issue instrumentation,
and documents the pattern in docs/observability.md.
Gitea may return JSON null for an issue's `labels`, `assignees`, or
`user` fields. `dict.get(key, [])` returns None when the key is present
with a null value (the default is only used for missing keys), so the
list comprehensions raised `'NoneType' object is not iterable` for
otherwise-valid issues. Coalesce with `or []` / `or {}` so empty/null
collections normalize to empty results.
Adds a regression test covering all three null fields.
Two related issues made the connected MCP server return a bare "Internal
server error" for tools that need real Gitea API access (e.g.
list_repositories), while public-repo-by-path reads worked:
1. Gitea OIDC access tokens only carry openid/profile/email and cannot call
the repository REST API, so pure-OAuth mode fails for most tools. A service
PAT (GITEA_TOKEN) is required in practice; per-user permission is still
enforced before each call, so this does not weaken authorization.
2. The tool handlers caught GiteaError broadly and re-raised it as RuntimeError.
Because GiteaAuthenticationError/GiteaAuthorizationError subclass GiteaError,
a clean 401/403 was masked as a generic internal error and the server's
re-authorization guidance never fired.
Changes:
- read_tools.py / repository.py / write_tools.py: re-raise the auth/authz
subclasses before the broad GiteaError catch so server.py returns actionable
guidance instead of a generic 500.
- .env.example + README.md: document GITEA_TOKEN as a least-privilege bot PAT,
explain why it's needed and that OAuth remains authoritative, and note that
list_repositories is intentionally unavailable in service-PAT mode.
- tests: assert tool handlers propagate auth errors unwrapped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>