Let one MCP server select among named Gitea runtime profiles from a JSON file
instead of editing code or juggling many .env files:
GITEA_MCP_CONFIG=/path/to/gitea-mcp.json
GITEA_MCP_PROFILE=dev
- New gitea_config.py: load/validate the JSON, select the named profile, and
resolve its token by env-var reference. Profiles supply base_url,
profile_name, token_env, owner/repo, allowed/forbidden operations, and audit
label.
- gitea_auth.get_profile() now overlays env over the selected JSON profile:
explicit env vars win, the JSON profile fills only what env leaves unset.
- gitea_auth.get_auth_header() gains a JSON token_env fallback after explicit
env tokens (env still wins).
Security / safety:
- Tokens are referenced by env-var NAME (token_env); an inline "token" is
rejected and never echoed. The value is never stored in or returned as
profile metadata.
- Fail-safe errors: missing file / invalid JSON / unknown or unset selected
profile raise a clear ConfigError that never prints file contents or tokens
(JSONDecodeError context is suppressed so the raw file text can't surface).
- No network calls during config parsing.
- Real config files are gitignored (gitea-mcp*.json), example kept.
Backwards compatible: with GITEA_MCP_CONFIG unset, behaviour is exactly the
prior env-only behaviour (all existing get_profile/get_auth_header tests pass
unchanged).
Docs: README JSON-profiles section + env table rows, .env.example placeholders,
gitea-mcp.example.json.
Tests: tests/test_config.py (22 cases) — env-only, selection, multiple
profiles, env-override precedence, missing file, invalid JSON, missing/unset
profile, inline-token rejection + redaction, and no-network-during-parse.
Refs #10. Note: issue #19 (env-based profiles) was already implemented and
closed; this JSON-file capability is adjacent new scope tracked under the
roadmap rather than reopening #19.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add durable, opt-in audit logging for every mutating Gitea MCP action so an
operator can see which execution profile and authenticated Gitea user
performed (or was blocked from / failed) each mutation.
- New gitea_audit.py: pure, no-network module — recursive secret redaction
(token/password/authorization keys; token/Basic/Bearer value runs),
build_event (timestamp, action, result, profile, audit label, authenticated
username, repo, issue/PR, target branch, head SHA, redacted request
metadata), and an append-only JSON Lines sink.
- mcp_server.py: _audit helper + _audited context manager (simple mutations)
and an _audit_pr_result decorator (gated review/merge tools, reading their
own result dict) wired into create_issue, create_pr, edit_pr, close_issue,
commit_files, delete_branch, create_label, set_issue_labels, mark_issue
(label/unlabel), gitea_submit_pr_review, and gitea_merge_pr.
- Outcomes recorded as allowed/blocked/failed/succeeded; blocked and failed
eligibility checks are logged, not just successes.
Off by default: records are written only when GITEA_AUDIT_LOG is set. When it
is unset every audit path short-circuits — no records, no extra API calls — so
existing tool behaviour and API call sequences are unchanged. Auditing never
raises; sink writes are best-effort. Tokens are never written.
Docs: README env table + audit note, .env.example placeholder.
Tests: tests/test_audit.py (19 cases) — redaction, event build, sink writes,
per-tool success/failure/blocked records, secret-free output, off-by-default
no-op, and audit-failure-never-breaks-action.
Closes#18
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a read-only MCP tool that reports the active runtime execution
profile so an LLM can inspect what the current process is configured to
do before deciding whether to attempt an action later.
- gitea_get_profile: returns profile_name, allowed/forbidden operation
categories, audit_label, token_source_name (a NAME, never a value),
base_url, remote, resolved server, and — optionally — the verified
authenticated username. Identity resolution fails soft and marks
identity_status (verified/unknown/unavailable/not_resolved); the
profile config is always returned. Never mutates Gitea.
- gitea_auth.get_profile(): extended with forbidden_operations,
audit_label, token_source_name from env (non-secret metadata).
- .env.example / README: document the new optional metadata vars and
the discovery tool.
- tests: metadata parsing, verified/unavailable/unknown identity paths,
skip-identity, and secret-redaction.
Read-only. No token exposure. No multi-token switching. No PR
eligibility, review, or merge workflow. No Jenkins/Ops/GlitchTip/
Release/deploy behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow the same MCP server to run as separate MCP entries, each with its
own token and profile name, so roles stay task-scoped (the profile is
the role, not the LLM).
- gitea_auth.get_profile(): reads GITEA_PROFILE_NAME,
GITEA_ALLOWED_OPERATIONS, GITEA_BASE_URL as non-secret metadata.
Never reads/returns/logs the token.
- gitea_whoami now surfaces the safe profile metadata (name + allowed
operations) alongside identity; token still never exposed.
- .env.example: placeholder-only template for a runtime profile.
- .gitignore: track .env.example while keeping real .env* ignored.
- README: document multiple env-configured MCP entries.
- tests: profile defaults/parsing, token-never-included, whoami surfaces
profile without leaking token.
One token + one profile per process. No multi-token switching in a
single runtime. No approve/merge/eligibility workflow. No
Jenkins/Ops/GlitchTip/Release/deploy behavior. No real secrets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>