Fix gitea_api_call to accept both string and dict for params/body
All checks were successful
Build and Push Gitea MCP Docker Image / build (push) Successful in 17s

The gitea_api_call tool was failing when MCP clients passed JSON objects
for the body or params parameters. The issue was that the tool signature
only accepted string types, but MCP frameworks parse JSON strings into
dicts before passing them to the tool.

This fix updates the function signature to accept Union[str, dict] for
both params and body parameters, and handles both cases appropriately.

Fixes #1
This commit is contained in:
Ben
2025-12-26 20:02:26 +00:00
parent 77a945bd1e
commit f8eda211e6

123
server.py
View File

@@ -293,7 +293,9 @@ async def list_repo_commits(
if sha: if sha:
params["sha"] = sha params["sha"] = sha
result = await client.request("GET", f"/repos/{owner}/{repo}/commits", params=params) result = await client.request(
"GET", f"/repos/{owner}/{repo}/commits", params=params
)
return json.dumps(result) return json.dumps(result)
@@ -325,13 +327,17 @@ async def get_workflow_run_logs(
return json.dumps(tasks_result) return json.dumps(tasks_result)
# Find the run matching the run_number, or use the most recent # Find the run matching the run_number, or use the most recent
workflow_runs = tasks_result.get("workflow_runs", []) if isinstance(tasks_result, dict) else [] workflow_runs = (
tasks_result.get("workflow_runs", []) if isinstance(tasks_result, dict) else []
)
if not workflow_runs: if not workflow_runs:
return json.dumps({ return json.dumps(
{
"error": True, "error": True,
"message": f"No workflow runs found in repository {owner}/{repo}" "message": f"No workflow runs found in repository {owner}/{repo}",
}) }
)
target_run = None target_run = None
if run_number is None: if run_number is None:
@@ -345,11 +351,13 @@ async def get_workflow_run_logs(
break break
if not target_run: if not target_run:
return json.dumps({ return json.dumps(
{
"error": True, "error": True,
"message": f"Workflow run #{run_number} not found in repository {owner}/{repo}", "message": f"Workflow run #{run_number} not found in repository {owner}/{repo}",
"available_runs": [r.get("run_number") for r in workflow_runs[:10]] "available_runs": [r.get("run_number") for r in workflow_runs[:10]],
}) }
)
run_id = target_run.get("id") run_id = target_run.get("id")
run_status = target_run.get("status") run_status = target_run.get("status")
@@ -371,27 +379,33 @@ async def get_workflow_run_logs(
break break
if not job_id: if not job_id:
return json.dumps({ return json.dumps(
{
"run_number": run_number, "run_number": run_number,
"run_id": run_id, "run_id": run_id,
"status": run_status, "status": run_status,
"title": run_title, "title": run_title,
"error": "Could not find job ID for this run", "error": "Could not find job ID for this run",
"detail": "The job may still be queued or the run data is not available" "detail": "The job may still be queued or the run data is not available",
}) }
)
# Step 3: Fetch the logs # Step 3: Fetch the logs
logs_result = await client.request("GET", f"/repos/{owner}/{repo}/actions/jobs/{job_id}/logs") logs_result = await client.request(
"GET", f"/repos/{owner}/{repo}/actions/jobs/{job_id}/logs"
)
if isinstance(logs_result, dict) and logs_result.get("error"): if isinstance(logs_result, dict) and logs_result.get("error"):
return json.dumps({ return json.dumps(
{
"run_number": run_number, "run_number": run_number,
"run_id": run_id, "run_id": run_id,
"status": run_status, "status": run_status,
"title": run_title, "title": run_title,
"error": "Failed to fetch logs", "error": "Failed to fetch logs",
"detail": logs_result.get("message", "Unknown error") "detail": logs_result.get("message", "Unknown error"),
}) }
)
# Extract log content # Extract log content
log_content = "" log_content = ""
@@ -407,15 +421,36 @@ async def get_workflow_run_logs(
if run_status == "failure" and tail_lines > 0 and total_lines > tail_lines: if run_status == "failure" and tail_lines > 0 and total_lines > tail_lines:
# Error indicators to search for # Error indicators to search for
error_patterns = [ error_patterns = [
"", "Error:", "ERROR:", "error:", "FAILED", "Failed", "",
"fatal:", "FATAL:", "Exception:", "exception:", "Error:",
"Cannot find", "not found", "No such file", "ERROR:",
"Permission denied", "command not found", "error:",
"npm ERR!", "pnpm ERR!", "yarn error", "FAILED",
"TypeScript error", "TS2", "TS1", # TypeScript errors "Failed",
"SyntaxError", "ReferenceError", "TypeError", "fatal:",
"Build failed", "build failed", "Compilation failed", "FATAL:",
"exit code 1", "exit code 2", "exited with", "Exception:",
"exception:",
"Cannot find",
"not found",
"No such file",
"Permission denied",
"command not found",
"npm ERR!",
"pnpm ERR!",
"yarn error",
"TypeScript error",
"TS2",
"TS1", # TypeScript errors
"SyntaxError",
"ReferenceError",
"TypeError",
"Build failed",
"build failed",
"Compilation failed",
"exit code 1",
"exit code 2",
"exited with",
] ]
# Find lines containing errors # Find lines containing errors
@@ -454,26 +489,37 @@ async def get_workflow_run_logs(
output_parts.append(lines[idx]) output_parts.append(lines[idx])
prev_idx = idx prev_idx = idx
log_content = f"... (showing {len(sorted_indices)} error-relevant lines of {total_lines} total) ...\n" + "\n".join(output_parts) log_content = (
f"... (showing {len(sorted_indices)} error-relevant lines of {total_lines} total) ...\n"
+ "\n".join(output_parts)
)
else: else:
# No errors found, fall back to tail # No errors found, fall back to tail
lines = lines[-tail_lines:] lines = lines[-tail_lines:]
log_content = f"... (showing last {tail_lines} of {total_lines} lines) ...\n" + "\n".join(lines) log_content = (
f"... (showing last {tail_lines} of {total_lines} lines) ...\n"
+ "\n".join(lines)
)
elif tail_lines > 0 and total_lines > tail_lines: elif tail_lines > 0 and total_lines > tail_lines:
# Not a failure or no smart extraction, just tail # Not a failure or no smart extraction, just tail
lines = lines[-tail_lines:] lines = lines[-tail_lines:]
log_content = f"... (showing last {tail_lines} of {total_lines} lines) ...\n" + "\n".join(lines) log_content = (
f"... (showing last {tail_lines} of {total_lines} lines) ...\n"
+ "\n".join(lines)
)
else: else:
log_content = "\n".join(lines) log_content = "\n".join(lines)
return json.dumps({ return json.dumps(
{
"run_number": run_number, "run_number": run_number,
"run_id": run_id, "run_id": run_id,
"status": run_status, "status": run_status,
"title": run_title, "title": run_title,
"job_id": job_id, "job_id": job_id,
"logs": log_content "logs": log_content,
}) }
)
# ============================================================================= # =============================================================================
@@ -485,8 +531,8 @@ async def get_workflow_run_logs(
async def gitea_api_call( async def gitea_api_call(
endpoint: str, endpoint: str,
method: str = "GET", method: str = "GET",
params: str = "{}", params: str | dict[str, Any] = "{}",
body: str = "{}", body: str | dict[str, Any] = "{}",
) -> str: ) -> str:
"""Execute a raw API call to Gitea. """Execute a raw API call to Gitea.
@@ -497,16 +543,25 @@ async def gitea_api_call(
Args: Args:
endpoint: API endpoint path (e.g., '/repos/owner/repo/releases') endpoint: API endpoint path (e.g., '/repos/owner/repo/releases')
method: HTTP method (GET, POST, PUT, PATCH, DELETE) method: HTTP method (GET, POST, PUT, PATCH, DELETE)
params: JSON string of query parameters (optional) params: JSON string or dict of query parameters (optional)
body: JSON string of request body for POST/PUT/PATCH (optional) body: JSON string or dict of request body for POST/PUT/PATCH (optional)
Example: Example:
gitea_api_call('/repos/myorg/myrepo/releases', 'POST', gitea_api_call('/repos/myorg/myrepo/releases', 'POST',
body='{{"tag_name": "v1.0.0", "name": "Release 1.0"}}') body='{{"tag_name": "v1.0.0", "name": "Release 1.0"}}')
""".format(base_url=GITEA_URL) """.format(base_url=GITEA_URL)
try: try:
# Handle params: accept both string and dict
if isinstance(params, str):
params_dict = json.loads(params) if params else {} params_dict = json.loads(params) if params else {}
else:
params_dict = params or {}
# Handle body: accept both string and dict
if isinstance(body, str):
body_dict = json.loads(body) if body else {} body_dict = json.loads(body) if body else {}
else:
body_dict = body or {}
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return json.dumps({"error": True, "message": f"Invalid JSON: {e}"}) return json.dumps({"error": True, "message": f"Invalid JSON: {e}"})