feat: add read-only Gitea task capability resolver tool (#141)

This commit is contained in:
2026-07-05 03:21:51 -04:00
parent 3c1ab87e1b
commit 8ef9129e44
2 changed files with 284 additions and 0 deletions
+146
View File
@@ -2742,6 +2742,152 @@ def gitea_mirror_refs(
"return_code": result.returncode,
}
@mcp.tool()
def gitea_resolve_task_capability(
task: str,
remote: str = "dadeschools",
host: str | None = None,
) -> dict:
"""Read-only: Resolve which capability, profile, and namespace is required for a Gitea task.
Helps the client or LLM determine the correct namespace or profile before acting,
and returns exact next action instructions if the current session is not authorized.
Args:
task: The task/action to check (e.g. review_pr, create_issue).
remote: Known remote instance name.
host: Optional override for the Gitea host.
"""
TASK_MAP = {
"create_issue": {
"permission": "gitea.issue.create",
"role": "author",
},
"comment_issue": {
"permission": "gitea.issue.comment",
"role": "author",
},
"close_issue": {
"permission": "gitea.issue.close",
"role": "author",
},
"claim_issue": {
"permission": "gitea.issue.write",
"role": "author",
},
"create_branch": {
"permission": "gitea.branch.create",
"role": "author",
},
"push_branch": {
"permission": "gitea.branch.push",
"role": "author",
},
"create_pr": {
"permission": "gitea.pr.create",
"role": "author",
},
"comment_pr": {
"permission": "gitea.pr.comment",
"role": "author",
},
"review_pr": {
"permission": "gitea.pr.review",
"role": "reviewer",
},
"merge_pr": {
"permission": "gitea.pr.merge",
"role": "reviewer",
},
"delete_branch": {
"permission": "gitea.branch.delete",
"role": "author",
},
}
if task not in TASK_MAP:
raise ValueError(f"Unknown task/action: '{task}' (fail closed)")
required_permission = TASK_MAP[task]["permission"]
required_role = TASK_MAP[task]["role"]
profile = get_profile()
config = gitea_config.load_config()
h = host or (REMOTES.get(remote, {}).get("host") if remote in REMOTES else None)
username = _authenticated_username(h) if h else None
# Load active permissions
active_allowed = profile.get("allowed_operations") or []
active_forbidden = profile.get("forbidden_operations") or []
# Check if allowed in current session
allowed_in_current_session, _ = gitea_config.check_operation(
required_permission, active_allowed, active_forbidden
)
# Find matching configured profiles
matching_profiles = []
if config and "profiles" in config:
for p_name, p_data in config["profiles"].items():
p_allowed = p_data.get("allowed_operations") or []
p_forbidden = p_data.get("forbidden_operations") or []
p_allowed_n = []
for op in p_allowed:
try:
p_allowed_n.append(gitea_config.normalize_operation(op))
except Exception:
pass
p_forbidden_n = []
for op in p_forbidden:
try:
p_forbidden_n.append(gitea_config.normalize_operation(op))
except Exception:
pass
ok, _ = gitea_config.check_operation(required_permission, p_allowed_n, p_forbidden_n)
if ok:
matching_profiles.append(p_name)
switching = gitea_config.is_runtime_switching_enabled()
different_namespace_required = False
next_safe_action = "None; ready for operations."
if not allowed_in_current_session:
if switching:
different_namespace_required = False
next_safe_action = f"Switch to a profile that has the required permission by calling gitea_activate_profile (matching configured profiles: {matching_profiles})."
else:
different_namespace_required = True
if required_role == "reviewer":
next_safe_action = (
"Switch to the reviewer MCP session (e.g. gitea-reviewer) which has reviewer permissions configured, "
"or ask the operator to update GITEA_MCP_PROFILE to a reviewer profile."
)
elif required_role == "author":
next_safe_action = (
"Switch to the author MCP session (e.g. gitea-author) which has author permissions configured, "
"or ask the operator to update GITEA_MCP_PROFILE to an author profile."
)
else:
next_safe_action = (
f"Relaunch the server with GITEA_MCP_PROFILE set to a profile that has the required permission (e.g. one of: {matching_profiles}) "
"or use the corresponding MCP namespace."
)
return {
"requested_task": task,
"required_operation_permission": required_permission,
"required_role_kind": required_role,
"active_profile": profile["profile_name"],
"active_identity": username,
"active_profile_allowed_operations": active_allowed,
"allowed_in_current_session": allowed_in_current_session,
"matching_configured_profile": matching_profiles,
"runtime_switching_supported": switching,
"different_mcp_namespace_required": different_namespace_required,
"exact_safe_next_action": next_safe_action,
}
# ── Entry point ───────────────────────────────────────────────────────────────