feat: JSON multi-profile runtime config for Gitea MCP (roadmap #10)

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>
This commit is contained in:
2026-07-01 22:44:31 -04:00
parent d4251c5c47
commit 3aaba73127
7 changed files with 507 additions and 10 deletions
+37
View File
@@ -169,6 +169,8 @@ Recognized environment fields (see [`.env.example`](.env.example) for placeholde
| `GITEA_TOKEN_SOURCE` | Optional *name* of the token source (e.g. an env var name). A name only — never the token value. |
| `GITEA_BASE_URL` | Optional informational base URL. |
| `GITEA_AUDIT_LOG` | Optional path to an audit log file. When set, mutating actions append one redacted JSON record each (profile + authenticated user + outcome). Unset ⇒ auditing off (no records, no extra API calls). |
| `GITEA_MCP_CONFIG` | Optional path to a JSON file defining multiple named runtime profiles. Unset ⇒ pure env behaviour. |
| `GITEA_MCP_PROFILE` | Name of the profile (from `GITEA_MCP_CONFIG`) to activate for this runtime. |
Notes:
@@ -187,6 +189,41 @@ Notes:
branch and head SHA where applicable — when `GITEA_AUDIT_LOG` is set. Auditing
is off by default and never adds API calls or breaks the action when off.
See [`gitea_audit.py`](gitea_audit.py).
**JSON runtime profiles (roadmap #10).** Instead of juggling many `.env` files,
one server can pick a named profile from a JSON file (see
[`gitea-mcp.example.json`](gitea-mcp.example.json), loaded by
[`gitea_config.py`](gitea_config.py)):
```bash
export GITEA_MCP_CONFIG=/path/to/gitea-mcp.json
export GITEA_MCP_PROFILE=dev
```
```json
{
"profiles": {
"dev": {
"base_url": "https://gitea.dev.example",
"profile_name": "gitea-author",
"token_env": "GITEA_TOKEN_DEV",
"allowed_operations": ["read", "pr.create"],
"audit_label": "dev-author"
}
}
}
```
- **Token by reference only:** a profile names the env var holding its token
(`token_env`); an inline `token` is rejected. The value is read from that env
var and never stored in or returned as profile metadata.
- **Precedence:** explicit env vars (`GITEA_PROFILE_NAME`, `GITEA_BASE_URL`,
`GITEA_ALLOWED_OPERATIONS`, `GITEA_TOKEN`, …) **override** the JSON profile;
the JSON profile only fills what the environment leaves unset.
- **Backwards compatible / fail-safe:** with `GITEA_MCP_CONFIG` unset, behaviour
is exactly env-only. A missing file, invalid JSON, or unknown/unset selected
profile raises a clear startup error that never prints file contents or
tokens. Parsing makes no network calls.
</details>
<details>