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
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:
163
server.py
163
server.py
@@ -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,
|
{
|
||||||
"message": f"No workflow runs found in repository {owner}/{repo}"
|
"error": True,
|
||||||
})
|
"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,
|
{
|
||||||
"message": f"Workflow run #{run_number} not found in repository {owner}/{repo}",
|
"error": True,
|
||||||
"available_runs": [r.get("run_number") for r in workflow_runs[:10]]
|
"message": f"Workflow run #{run_number} not found in repository {owner}/{repo}",
|
||||||
})
|
"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_id": run_id,
|
"run_number": run_number,
|
||||||
"status": run_status,
|
"run_id": run_id,
|
||||||
"title": run_title,
|
"status": run_status,
|
||||||
"error": "Could not find job ID for this run",
|
"title": run_title,
|
||||||
"detail": "The job may still be queued or the run data is not available"
|
"error": "Could not find job ID for this run",
|
||||||
})
|
"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_id": run_id,
|
"run_number": run_number,
|
||||||
"status": run_status,
|
"run_id": run_id,
|
||||||
"title": run_title,
|
"status": run_status,
|
||||||
"error": "Failed to fetch logs",
|
"title": run_title,
|
||||||
"detail": logs_result.get("message", "Unknown error")
|
"error": "Failed to fetch logs",
|
||||||
})
|
"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_id": run_id,
|
"run_number": run_number,
|
||||||
"status": run_status,
|
"run_id": run_id,
|
||||||
"title": run_title,
|
"status": run_status,
|
||||||
"job_id": job_id,
|
"title": run_title,
|
||||||
"logs": log_content
|
"job_id": job_id,
|
||||||
})
|
"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:
|
||||||
params_dict = json.loads(params) if params else {}
|
# Handle params: accept both string and dict
|
||||||
body_dict = json.loads(body) if body else {}
|
if isinstance(params, str):
|
||||||
|
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 {}
|
||||||
|
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}"})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user