Skip to content

Commit 17e1973

Browse files
Vijay-DukeVijay Iyengar
andauthored
fix: Add missing GitLab client methods to fix AttributeErrors (#18)
Added the following missing methods to GitLabClient: - get_repository_tree: List files and directories in repository - search_in_project: Search within a project (blobs, issues, commits, etc.) - summarize_merge_request: Generate AI-friendly MR summary - batch_operations: Execute multiple operations in batch These methods were being called by tool handlers but didn't exist, causing AttributeError exceptions. The safe_preview_commit method already exists from a previous PR. Fixes: - gitlab_list_repository_tree functionality - gitlab_list_commits functionality (handler was calling get_commits) - gitlab_search_in_project functionality - gitlab_summarize_merge_request functionality - gitlab_batch_operations functionality Co-authored-by: Vijay Iyengar <Vijay.Iyengar@team.belong.com.au>
1 parent d28e15f commit 17e1973

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

src/mcp_gitlab/gitlab_client.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,6 +2552,273 @@ def get_user_resolved_threads(self, username: str, project_id: Optional[str] = N
25522552
}
25532553
}
25542554

2555+
@retry_on_error()
2556+
def get_repository_tree(self, project_id: str, path: str = "", ref: Optional[str] = None,
2557+
recursive: bool = False) -> Dict[str, Any]:
2558+
"""Get repository tree (list of files and directories).
2559+
2560+
Args:
2561+
project_id: The ID or path of the project
2562+
path: The path inside repository to list
2563+
ref: The name of a repository branch or tag
2564+
recursive: Boolean value to get a recursive tree
2565+
2566+
Returns:
2567+
Dict containing repository tree items
2568+
"""
2569+
try:
2570+
project = self.gl.projects.get(project_id)
2571+
kwargs = {"path": path, "recursive": recursive}
2572+
if ref:
2573+
kwargs["ref"] = ref
2574+
2575+
tree = project.repository_tree(**kwargs)
2576+
2577+
return {
2578+
"tree": [
2579+
{
2580+
"id": item.get("id"),
2581+
"name": item.get("name"),
2582+
"type": item.get("type"), # "tree" for directory, "blob" for file
2583+
"path": item.get("path"),
2584+
"mode": item.get("mode"),
2585+
}
2586+
for item in tree
2587+
],
2588+
"project_id": project_id,
2589+
"path": path,
2590+
"ref": ref,
2591+
}
2592+
except gitlab.exceptions.GitlabGetError as e:
2593+
return {"error": f"Failed to get repository tree: {str(e)}"}
2594+
2595+
@retry_on_error()
2596+
def search_in_project(self, project_id: str, scope: str, search: str,
2597+
per_page: int = DEFAULT_PAGE_SIZE, page: int = 1) -> Dict[str, Any]:
2598+
"""Search within a project.
2599+
2600+
Args:
2601+
project_id: The ID or path of the project
2602+
scope: Scope of search (blobs, issues, merge_requests, milestones, notes, wiki_blobs, commits)
2603+
search: Search query
2604+
per_page: Number of items per page
2605+
page: Page number
2606+
2607+
Returns:
2608+
Dict containing search results
2609+
"""
2610+
try:
2611+
project = self.gl.projects.get(project_id)
2612+
results = project.search(scope, search, get_all=False, per_page=per_page, page=page)
2613+
2614+
# Format results based on scope
2615+
formatted_results = []
2616+
for item in results:
2617+
if scope == "blobs":
2618+
formatted_results.append({
2619+
"basename": getattr(item, "basename", None),
2620+
"data": getattr(item, "data", None),
2621+
"path": getattr(item, "path", None),
2622+
"filename": getattr(item, "filename", None),
2623+
"id": getattr(item, "id", None),
2624+
"ref": getattr(item, "ref", None),
2625+
"startline": getattr(item, "startline", None),
2626+
"project_id": getattr(item, "project_id", None),
2627+
})
2628+
elif scope == "commits":
2629+
formatted_results.append({
2630+
"id": getattr(item, "id", None),
2631+
"short_id": getattr(item, "short_id", None),
2632+
"title": getattr(item, "title", None),
2633+
"message": getattr(item, "message", None),
2634+
"author_name": getattr(item, "author_name", None),
2635+
"created_at": getattr(item, "created_at", None),
2636+
})
2637+
elif scope in ["issues", "merge_requests"]:
2638+
formatted_results.append({
2639+
"id": getattr(item, "id", None),
2640+
"iid": getattr(item, "iid", None),
2641+
"title": getattr(item, "title", None),
2642+
"description": getattr(item, "description", None),
2643+
"state": getattr(item, "state", None),
2644+
"created_at": getattr(item, "created_at", None),
2645+
"updated_at": getattr(item, "updated_at", None),
2646+
"web_url": getattr(item, "web_url", None),
2647+
})
2648+
else:
2649+
# Generic formatting for other scopes
2650+
formatted_results.append({
2651+
k: getattr(item, k, None)
2652+
for k in dir(item)
2653+
if not k.startswith('_')
2654+
})
2655+
2656+
pagination = {
2657+
"page": page,
2658+
"per_page": per_page,
2659+
"total": getattr(results, "total", None),
2660+
"total_pages": getattr(results, "total_pages", None),
2661+
"next_page": getattr(results, "next_page", None),
2662+
"prev_page": getattr(results, "prev_page", None),
2663+
}
2664+
2665+
return {
2666+
"results": formatted_results,
2667+
"pagination": pagination,
2668+
"scope": scope,
2669+
"search": search,
2670+
"project_id": project_id,
2671+
}
2672+
except gitlab.exceptions.GitlabGetError as e:
2673+
return {"error": f"Search failed: {str(e)}"}
2674+
except Exception as e:
2675+
return {"error": f"Failed to search in project: {str(e)}"}
2676+
2677+
@retry_on_error()
2678+
def summarize_merge_request(self, project_id: str, mr_iid: int, max_length: int = 500) -> Dict[str, Any]:
2679+
"""Generate an AI-friendly summary of a merge request.
2680+
2681+
This method retrieves MR details, changes, and discussions, then formats them
2682+
into a concise summary suitable for LLM context.
2683+
2684+
Args:
2685+
project_id: The ID or path of the project
2686+
mr_iid: The IID of the merge request
2687+
max_length: Maximum length for description/discussion summary
2688+
2689+
Returns:
2690+
Dict containing structured MR summary with key information
2691+
"""
2692+
try:
2693+
project = self.gl.projects.get(project_id)
2694+
mr = project.mergerequests.get(mr_iid)
2695+
2696+
# Get MR changes
2697+
changes = mr.changes()
2698+
2699+
# Get discussions
2700+
discussions = mr.discussions.list(get_all=True)
2701+
2702+
# Summarize files changed
2703+
files_changed = []
2704+
for change in changes.get("changes", []):
2705+
files_changed.append({
2706+
"path": change.get("new_path"),
2707+
"additions": change.get("diff", "").count("\n+"),
2708+
"deletions": change.get("diff", "").count("\n-"),
2709+
})
2710+
2711+
# Summarize discussions
2712+
discussion_summary = []
2713+
for discussion in discussions[:5]: # Limit to first 5 discussions
2714+
notes = discussion.attributes.get("notes", [])
2715+
if notes:
2716+
first_note = notes[0]
2717+
discussion_summary.append({
2718+
"author": first_note.get("author", {}).get("username"),
2719+
"created_at": first_note.get("created_at"),
2720+
"resolved": discussion.attributes.get("resolved", False),
2721+
"note_count": len(notes),
2722+
})
2723+
2724+
# Create summary
2725+
description = mr.description or ""
2726+
if len(description) > max_length:
2727+
description = description[:max_length] + "..."
2728+
2729+
return {
2730+
"mr_iid": mr_iid,
2731+
"title": mr.title,
2732+
"state": mr.state,
2733+
"author": mr.author.get("username"),
2734+
"created_at": mr.created_at,
2735+
"updated_at": mr.updated_at,
2736+
"source_branch": mr.source_branch,
2737+
"target_branch": mr.target_branch,
2738+
"description_summary": description,
2739+
"files_changed_count": len(files_changed),
2740+
"files_changed_sample": files_changed[:10], # First 10 files
2741+
"additions_total": sum(f["additions"] for f in files_changed),
2742+
"deletions_total": sum(f["deletions"] for f in files_changed),
2743+
"discussion_count": len(discussions),
2744+
"discussions_summary": discussion_summary,
2745+
"merge_status": mr.merge_status,
2746+
"has_conflicts": mr.has_conflicts,
2747+
"labels": mr.labels,
2748+
"web_url": mr.web_url,
2749+
}
2750+
except gitlab.exceptions.GitlabGetError as e:
2751+
return {"error": f"Failed to get merge request: {str(e)}"}
2752+
2753+
@retry_on_error()
2754+
def batch_operations(self, project_id: str, operations: List[Dict[str, Any]],
2755+
stop_on_error: bool = True) -> Dict[str, Any]:
2756+
"""Execute multiple operations in batch.
2757+
2758+
Args:
2759+
project_id: The ID or path of the project
2760+
operations: List of operations to execute
2761+
stop_on_error: Whether to stop on first error
2762+
2763+
Returns:
2764+
Dict containing results of all operations
2765+
"""
2766+
results = []
2767+
2768+
for i, operation in enumerate(operations):
2769+
try:
2770+
op_type = operation.get("type")
2771+
op_params = operation.get("params", {})
2772+
2773+
# Add project_id to params if not present
2774+
if "project_id" not in op_params:
2775+
op_params["project_id"] = project_id
2776+
2777+
# Execute operation based on type
2778+
if op_type == "get_issue":
2779+
result = self.get_issue(**op_params)
2780+
elif op_type == "get_merge_request":
2781+
result = self.get_merge_request(**op_params)
2782+
elif op_type == "list_issues":
2783+
result = self.list_issues(**op_params)
2784+
elif op_type == "list_merge_requests":
2785+
result = self.list_merge_requests(**op_params)
2786+
elif op_type == "get_file_content":
2787+
result = self.get_file_content(**op_params)
2788+
elif op_type == "get_commits":
2789+
result = self.get_commits(**op_params)
2790+
else:
2791+
result = {"error": f"Unknown operation type: {op_type}"}
2792+
2793+
results.append({
2794+
"index": i,
2795+
"operation": op_type,
2796+
"success": "error" not in result,
2797+
"result": result,
2798+
})
2799+
2800+
if stop_on_error and "error" in result:
2801+
break
2802+
2803+
except Exception as e:
2804+
error_result = {
2805+
"index": i,
2806+
"operation": operation.get("type"),
2807+
"success": False,
2808+
"result": {"error": str(e)},
2809+
}
2810+
results.append(error_result)
2811+
2812+
if stop_on_error:
2813+
break
2814+
2815+
return {
2816+
"operations_count": len(operations),
2817+
"executed_count": len(results),
2818+
"success_count": sum(1 for r in results if r["success"]),
2819+
"results": results,
2820+
}
2821+
25552822

25562823
__all__ = ["GitLabClient", "GitLabConfig"]
25572824

0 commit comments

Comments
 (0)