feat: load profiles.json v2 contexts shape with enabled enforcement and LLM-safe output (#120)
Support the canonical contexts-shape version 2 config (contexts / profiles / projects / rules) alongside the existing environments shape and v1: - Require a boolean 'enabled' on every context, profile, service, and project. Disabled entries are surfaced in audits but fail closed at selection/resolution — never a silent fallback to another profile, service, or credential source. - Resolve the active identity from GITEA_MCP_PROFILE via the existing select_profile path; profile base_url falls back to the context's enabled gitea block. - Add resolve_service() and project_for_path() for context service and project-to-context resolution (internal use; fail closed on disabled). - get_auth_header now propagates ConfigError when GITEA_MCP_CONFIG is set instead of silently degrading to Basic auth. - Hide endpoint URLs and keychain ids from normal LLM-facing output: gitea_whoami / gitea_get_profile report logical names and auth status only; new gitea_audit_config tool reports enabled/disabled state and safe one-line service summaries. The GITEA_MCP_REVEAL_ENDPOINTS opt-in (and 'python3 gitea_config.py audit --reveal-endpoints' locally) restores endpoints and auth source names for admin diagnostics; token values are never printed on any path. - Ship gitea-mcp.v2-contexts.example.json (synthetic values) and validate it in tests. Implements #120 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+62
-10
@@ -43,6 +43,16 @@ from gitea_auth import ( # noqa: E402
|
||||
get_profile,
|
||||
)
|
||||
import gitea_audit # noqa: E402
|
||||
import gitea_config # noqa: E402
|
||||
|
||||
|
||||
def _reveal_endpoints() -> bool:
|
||||
"""Admin/debug opt-in (#120): include endpoint URLs and token source
|
||||
names in tool output. Off by default so normal LLM-facing responses
|
||||
expose only logical names and status. Never affects token values, which
|
||||
are excluded on every path."""
|
||||
return (os.environ.get("GITEA_MCP_REVEAL_ENDPOINTS") or "").strip().lower() \
|
||||
in ("1", "true", "yes")
|
||||
|
||||
mcp = FastMCP("gitea-tools", instructions=(
|
||||
"Gitea issue tracker and PR management for dadeschools and prgs instances. "
|
||||
@@ -1382,21 +1392,26 @@ def gitea_whoami(
|
||||
"Verify the configured token is valid for this instance."
|
||||
)
|
||||
# Runtime profile metadata is non-secret (name + allowed op categories).
|
||||
# The token is resolved separately and is never included here.
|
||||
# The token is resolved separately and is never included here. Endpoint
|
||||
# URLs stay out of normal LLM-facing output (#120): the logical remote
|
||||
# name is the addressing surface; 'server' appears only under the
|
||||
# GITEA_MCP_REVEAL_ENDPOINTS admin opt-in.
|
||||
profile = get_profile()
|
||||
return {
|
||||
result = {
|
||||
"authenticated": True,
|
||||
"username": data.get("login"),
|
||||
"display_name": data.get("full_name") or None,
|
||||
"user_id": data.get("id"),
|
||||
"email": data.get("email") or None,
|
||||
"server": f"https://{h}",
|
||||
"remote": remote,
|
||||
"profile": {
|
||||
"profile_name": profile["profile_name"],
|
||||
"allowed_operations": profile["allowed_operations"],
|
||||
},
|
||||
}
|
||||
if _reveal_endpoints():
|
||||
result["server"] = f"https://{h}"
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1427,9 +1442,11 @@ def gitea_get_profile(
|
||||
|
||||
Read-only. Reports the non-secret configuration of the running MCP
|
||||
process (profile name, allowed/forbidden operation categories, audit
|
||||
label, token *source name*, base URL) plus the resolved server for the
|
||||
given remote. Optionally resolves the authenticated username via
|
||||
``gitea_whoami``'s endpoint so an LLM can see who this runtime acts as.
|
||||
label, auth *status*). Endpoint URLs and token source names are hidden
|
||||
from normal output (#120) and appear only under the
|
||||
GITEA_MCP_REVEAL_ENDPOINTS admin opt-in. Optionally resolves the
|
||||
authenticated username via ``gitea_whoami``'s endpoint so an LLM can see
|
||||
who this runtime acts as.
|
||||
|
||||
This tool never mutates Gitea and never approves, merges, comments, or
|
||||
creates anything. It never returns the token value, Authorization header,
|
||||
@@ -1447,18 +1464,25 @@ def gitea_get_profile(
|
||||
'verified', 'unknown', 'unavailable', or 'not_resolved'.
|
||||
"""
|
||||
profile = get_profile()
|
||||
reveal = _reveal_endpoints()
|
||||
result = {
|
||||
"profile_name": profile["profile_name"],
|
||||
"allowed_operations": profile["allowed_operations"],
|
||||
"forbidden_operations": profile["forbidden_operations"],
|
||||
"audit_label": profile["audit_label"],
|
||||
"token_source_name": profile["token_source_name"],
|
||||
"base_url": profile["base_url"],
|
||||
# Auth is reported as a status only (#120): the token source *name*
|
||||
# (env var name / keychain id) joins endpoint URLs behind the
|
||||
# GITEA_MCP_REVEAL_ENDPOINTS admin opt-in. Token values never appear.
|
||||
"auth_status": ("configured" if profile["token_source_name"]
|
||||
else "unconfigured"),
|
||||
"remote": remote if remote in REMOTES else None,
|
||||
"server": None,
|
||||
"authenticated_username": None,
|
||||
"identity_status": "not_resolved",
|
||||
}
|
||||
if reveal:
|
||||
result["token_source_name"] = profile["token_source_name"]
|
||||
result["base_url"] = profile["base_url"]
|
||||
result["server"] = None
|
||||
|
||||
if remote not in REMOTES:
|
||||
# Mark ambiguity rather than raising: the tool stays inspectable.
|
||||
@@ -1467,7 +1491,8 @@ def gitea_get_profile(
|
||||
return result
|
||||
|
||||
h = host or REMOTES[remote]["host"]
|
||||
result["server"] = f"https://{h}"
|
||||
if reveal:
|
||||
result["server"] = f"https://{h}"
|
||||
|
||||
if resolve_identity:
|
||||
try:
|
||||
@@ -1487,6 +1512,33 @@ def gitea_get_profile(
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_audit_config() -> dict:
|
||||
"""Audit the configured profiles/services: enabled state, no secrets.
|
||||
|
||||
Read-only and local-only: loads the canonical profiles.json named by
|
||||
GITEA_MCP_CONFIG and reports profile/service names, contexts, enabled
|
||||
state, capabilities, auth *status*, and one-line service summaries (e.g.
|
||||
``PRGS Jenkins: enabled, read-only, authenticated``). Disabled entries
|
||||
are listed so they can be audited, but the server refuses to act with
|
||||
them and never falls back to another profile or service.
|
||||
|
||||
Never includes endpoint URLs, keychain ids, token source names, or token
|
||||
values. Endpoint-revealing diagnostics exist only in the local admin CLI
|
||||
(``python3 gitea_config.py audit --reveal-endpoints``), never over MCP.
|
||||
"""
|
||||
config = gitea_config.load_config()
|
||||
if config is None:
|
||||
return {
|
||||
"configured": False,
|
||||
"message": "No GITEA_MCP_CONFIG configured; env-only mode.",
|
||||
}
|
||||
report = gitea_config.audit_config(config)
|
||||
report["configured"] = True
|
||||
report["summaries"] = gitea_config.service_summaries(config)
|
||||
return report
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_mark_issue(
|
||||
issue_number: int,
|
||||
|
||||
Reference in New Issue
Block a user