Skip to main content
Glama
main_tools.py52.6 kB
from functools import wraps from typing import Any, Callable, Dict, List, Literal, Optional from fastmcp import FastMCP from conduit.client.types import ( ManiphestSearchAttachments, ManiphestSearchConstraints, ManiphestTaskTransactionComment, ManiphestTaskTransactionDescription, ManiphestTaskTransactionOwner, ManiphestTaskTransactionPriority, ManiphestTaskTransactionProjectsAdd, ManiphestTaskTransactionProjectsRemove, ManiphestTaskTransactionProjectsSet, ManiphestTaskTransactionStatus, ManiphestTaskTransactionTitle, UserSearchAttachments, UserSearchConstraints, ) from conduit.client.unified import PhabricatorClient from conduit.tools.handlers import handle_api_errors # Pagination Functions def _apply_smart_pagination(data: List[Any], limit: int = None) -> dict: """ Apply smart pagination to data. Args: data: List of data items limit: Maximum number of items to return (optional) Returns: Paginated response with metadata """ if limit is None: limit = 100 # Default limit # Apply limit if data is larger than limit if len(data) > limit: paginated_data = data[:limit] has_more = True total_count = len(data) suggestion = f"Use pagination to retrieve remaining {total_count - limit} items" else: paginated_data = data has_more = False total_count = len(data) suggestion = None return { "data": paginated_data, "pagination": { "total": total_count, "returned": len(paginated_data), "has_more": has_more, }, "suggestion": suggestion, } def optimize_token_usage(func: Callable) -> Callable: """ Decorator to apply smart limits and truncation to search results. """ @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) # Apply smart limits to search results if isinstance(result, dict) and "data" in result: # Check if this is a search result that needs limits data = result["data"] if isinstance(data, list) and len(data) > 50: # Apply smart pagination optimized_result = _apply_smart_pagination( data, kwargs.get("limit", 100) ) result.update(optimized_result) return result return wrapper def _truncate_text_response(text: str, max_length: int = 2000) -> dict: """ Truncate long text responses with helpful guidance. Args: text: The text to truncate max_length: Maximum allowed length Returns: Truncated response with guidance """ if len(text) <= max_length: return {"content": text, "truncated": False} truncated_text = text[:max_length] remaining_length = len(text) - max_length return { "content": truncated_text, "truncated": True, "original_length": len(text), "remaining_length": remaining_length, "suggestion": f"Content was truncated. {remaining_length} characters remaining. Use specific search parameters to reduce results.", } def _add_pagination_metadata(result: dict, cursor: dict = None) -> dict: """ Add pagination metadata to search results. Args: result: Original search result cursor: Pagination cursor from API Returns: Result with enhanced pagination metadata """ if cursor: result["pagination"] = { "cursor": cursor, "has_more": cursor.get("after") is not None, "limit": cursor.get("limit", 100), } return result def register_tools( # noqa: C901 mcp: FastMCP, get_client_func: Callable[[], PhabricatorClient], ) -> None: """ Register all MCP tools with the FastMCP instance. Args: mcp: FastMCP instance to register tools with get_client_func: Function to get Phabricator client instance """ @mcp.tool() @handle_api_errors def pha_user_whoami() -> dict: """ Get the current user's information. Returns: User information """ client = get_client_func() result = client.user.whoami() return {"success": True, "user": result} @mcp.tool() @handle_api_errors @optimize_token_usage def pha_user_search( query_key: str = "", ids: Optional[List[int]] = None, phids: Optional[List[str]] = None, usernames: Optional[List[str]] = None, name_like: str = "", is_admin: bool = None, is_disabled: bool = None, is_bot: bool = None, is_mailing_list: bool = None, needs_approval: bool = None, mfa: bool = None, created_start: int = None, created_end: int = None, fulltext_query: str = "", order: str = "", include_availability: bool = False, limit: int = 100, ) -> dict: """ Search for users with advanced filtering capabilities. Args: query_key: Builtin query ("active", "admin", "all", "approval") ids: List of specific user IDs to search for phids: List of specific user PHIDs to search for usernames: List of exact usernames to find name_like: Find users whose usernames or real names contain this substring is_admin: Pass true to find only administrators, or false to omit administrators is_disabled: Pass true to find only disabled users, or false to omit disabled users is_bot: Pass true to find only bots, or false to omit bots is_mailing_list: Pass true to find only mailing lists, or false to omit mailing lists needs_approval: Pass true to find only users awaiting approval, or false to omit these users mfa: Pass true to find only users enrolled in MFA, or false to omit these users created_start: Unix timestamp - find users created after this time created_end: Unix timestamp - find users created before this time fulltext_query: Full-text search query string order: Result ordering ("newest", "oldest", "relevance") include_availability: Include user availability information in results limit: Maximum number of results to return (default: 100, max: 1000) Returns: Search results with user data and pagination metadata """ # Initialize None parameters to empty lists if ids is None: ids = [] if phids is None: phids = [] if usernames is None: usernames = [] client = get_client_func() # Build constraints constraints: UserSearchConstraints = {} if ids: constraints["ids"] = ids if phids: constraints["phids"] = phids if usernames: constraints["usernames"] = usernames if name_like: constraints["nameLike"] = name_like if is_admin is not None: constraints["isAdmin"] = is_admin if is_disabled is not None: constraints["isDisabled"] = is_disabled if is_bot is not None: constraints["isBot"] = is_bot if is_mailing_list is not None: constraints["isMailingList"] = is_mailing_list if needs_approval is not None: constraints["needsApproval"] = needs_approval if mfa is not None: constraints["mfa"] = mfa if created_start is not None: constraints["createdStart"] = created_start if created_end is not None: constraints["createdEnd"] = created_end if fulltext_query: constraints["query"] = fulltext_query # Build attachments attachments: UserSearchAttachments = {} if include_availability: attachments["availability"] = True # Call the search API result = client.user.search( query_key=query_key or None, constraints=constraints if constraints else None, attachments=attachments if attachments else None, order=order or None, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "users": result["data"], "cursor": result["cursor"]} @mcp.tool() @handle_api_errors def pha_task_create( title: str, description: str = "", owner_phid: str = "" ) -> dict: """ Create a new Phabricator task. Args: title: Task title description: Task description owner_phid: PHID of the user to assign this task to Returns: Created task information """ client = get_client_func() result = client.maniphest.create_task( title=title, description=description, owner_phid=owner_phid, ) return {"success": True, "task": result} @mcp.tool() @handle_api_errors def pha_task_get(task_id: str) -> dict: """ Get details of a specific Phabricator task Args: task_id: The numeric ID of the task to retrieve (e.g., 1234) Returns: Task details """ client = get_client_func() result = client.maniphest.get_task(task_id) return {"success": True, "task": result} @mcp.tool() @handle_api_errors def pha_task_update( task_id: str, title: Optional[str] = None, description: Optional[str] = None, priority: Optional[str] = None, status: Optional[str] = None, owner_phid: Optional[str] = None, projects_add: Optional[List[str]] = None, projects_remove: Optional[List[str]] = None, projects_set: Optional[List[str]] = None, ) -> dict: """ Update the metadata of a Phabricator task. Args: task_id: The ID, PHID of the task to update. title: The new title for the task. description: The new description for the task. priority: The new priority for the task. status: The new status for the task. owner_phid: The PHID of the new owner for the task. projects_add: List of project PHIDs to add the task to. projects_remove: List of project PHIDs to remove the task from. projects_set: List of project PHIDs to set (overwrites current projects). Returns: Success status. """ client = get_client_func() transactions = [] if title is not None: transactions.append( ManiphestTaskTransactionTitle(type="title", value=title) ) if description is not None: transactions.append( ManiphestTaskTransactionDescription( type="description", value=description ) ) if priority is not None: transactions.append( ManiphestTaskTransactionPriority(type="priority", value=priority) ) if status is not None: transactions.append( ManiphestTaskTransactionStatus(type="status", value=status) ) if owner_phid is not None: transactions.append( ManiphestTaskTransactionOwner(type="owner", value=owner_phid) ) if projects_add is not None: transactions.append( ManiphestTaskTransactionProjectsAdd( type="projects.add", value=projects_add ) ) if projects_remove is not None: transactions.append( ManiphestTaskTransactionProjectsRemove( type="projects.remove", value=projects_remove ) ) if projects_set is not None: transactions.append( ManiphestTaskTransactionProjectsSet( type="projects.set", value=projects_set ) ) client.maniphest.edit_task( object_identifier=task_id, transactions=transactions, ) return {"success": True} @mcp.tool() @handle_api_errors def pha_task_add_comment(task_id: str, comment: str) -> dict: """ Add a comment to a Phabricator task. Args: task_id: The ID, PHID of the task to add the comment to. comment: The content of the comment. Returns: Success status. """ client = get_client_func() client.maniphest.edit_task( object_identifier=task_id, transactions=[ ManiphestTaskTransactionComment( type="comment", value=comment, ) ], ) return {"success": True} @mcp.tool() @handle_api_errors def pha_task_get_personal( task_type: Literal["assigned", "authored"] = "assigned", include_projects: bool = True, include_subscribers: bool = False, limit: int = 50, ) -> dict: """ Get personal tasks assigned to or authored by the current user. Args: task_type: Type of tasks to retrieve ("assigned" or "authored") include_projects: Include project information in results include_subscribers: Include subscriber information in results limit: Maximum number of results to return Returns: Personal tasks based on the specified type """ client = get_client_func() attachments: ManiphestSearchAttachments = {} if include_projects: attachments["projects"] = True if include_subscribers: attachments["subscribers"] = True if task_type == "assigned": result = client.maniphest.search_assigned_tasks( attachments=attachments if attachments else None, limit=limit ) return {"success": True, "assigned_tasks": result} elif task_type == "authored": result = client.maniphest.search_authored_tasks( attachments=attachments if attachments else None, limit=limit ) return {"success": True, "authored_tasks": result} else: return { "success": False, "error": "Invalid task_type. Use 'assigned' or 'authored'", } @mcp.tool() @handle_api_errors def pha_task_update_relationships( task_id: str, relationship_type: Literal["subtask", "parent"], target_ids: str, ) -> dict: """ Update task relationships (subtasks or parents). Args: task_id: The PHID of the task to update (must be PHID format, not numeric ID) relationship_type: Type of relationship ("subtask" or "parent") target_ids: Comma-separated list of target task PHIDs (must be PHID format, not numeric IDs) Returns: Success status """ client = get_client_func() # Parse comma-separated target IDs target_list = [ target.strip() for target in target_ids.split(",") if target.strip() ] if not target_list: return {"success": False, "error": "No valid target IDs provided"} if relationship_type == "subtask": transaction_type = "subtasks.set" elif relationship_type == "parent": transaction_type = "parents.set" else: return { "success": False, "error": "Invalid relationship_type. Use 'subtask' or 'parent'", } client.maniphest.edit_task( object_identifier=task_id, transactions=[ { "type": transaction_type, "value": target_list, } ], ) return {"success": True} @mcp.tool() @handle_api_errors @optimize_token_usage def pha_task_search_advanced( query_key: str = "", assigned: Optional[List[str]] = None, author_phids: Optional[List[str]] = None, statuses: Optional[List[str]] = None, priorities: Optional[List[int]] = None, projects: Optional[List[str]] = None, subscribers: Optional[List[str]] = None, fulltext_query: str = "", has_parents: bool = None, has_subtasks: bool = None, created_after: int = None, created_before: int = None, modified_after: int = None, modified_before: int = None, order: str = "", include_subscribers: bool = False, include_projects: bool = False, include_columns: bool = False, limit: int = 100, preset: Literal[ "all", "assigned", "authored", "open", "high_priority", "recent" ] = None, ) -> dict: """ Advanced task search with filtering and preset options. Args: query_key: Builtin query ("assigned", "authored", "subscribed", "open", "all") assigned: List of usernames or PHIDs of assignees author_phids: List of PHIDs of task authors statuses: List of task statuses to filter by priorities: List of priority levels to filter by projects: List of project names or PHIDs to filter by subscribers: List of subscriber usernames or PHIDs fulltext_query: Full-text search query string has_parents: Filter by whether tasks have parent tasks has_subtasks: Filter by whether tasks have subtasks created_after: Unix timestamp - tasks created after this time created_before: Unix timestamp - tasks created before this time modified_after: Unix timestamp - tasks modified after this time modified_before: Unix timestamp - tasks modified before this time order: Result ordering ("priority", "updated", "newest", "oldest", "closed", "title", "relevance") include_subscribers: Include subscriber information in results include_projects: Include project information in results include_columns: Include workboard column information in results limit: Maximum number of results to return (default: 100, max: 1000) preset: Preset search configurations for common use cases Returns: Search results with task data and pagination metadata """ # Initialize None parameters to empty lists if assigned is None: assigned = [] if author_phids is None: author_phids = [] if statuses is None: statuses = [] if priorities is None: priorities = [] if projects is None: projects = [] if subscribers is None: subscribers = [] client = get_client_func() # Handle preset configurations if preset: if preset == "assigned": query_key = "assigned" if not assigned: # Get current user for assigned tasks user_info = client.user.whoami() assigned = [user_info["phid"]] elif preset == "authored": query_key = "authored" if not author_phids: # Get current user for authored tasks user_info = client.user.whoami() author_phids = [user_info["phid"]] elif preset == "high_priority": priorities = [90, 100] # High and Unbreak Now priorities order = "priority" elif preset == "recent": import time modified_after = int(time.time()) - (7 * 24 * 60 * 60) # Last 7 days order = "updated" elif preset == "open": statuses = ["open"] elif preset == "all": query_key = "all" # Build constraints constraints: ManiphestSearchConstraints = {} if assigned: constraints["assigned"] = assigned if author_phids: constraints["authorPHIDs"] = author_phids if statuses: constraints["statuses"] = statuses if priorities: constraints["priorities"] = priorities if projects: constraints["projects"] = projects if subscribers: constraints["subscribers"] = subscribers if fulltext_query: constraints["query"] = fulltext_query if has_parents is not None: constraints["hasParents"] = has_parents if has_subtasks is not None: constraints["hasSubtasks"] = has_subtasks if created_after: constraints["createdStart"] = created_after if created_before: constraints["createdEnd"] = created_before if modified_after: constraints["modifiedStart"] = modified_after if modified_before: constraints["modifiedEnd"] = modified_before # Build attachments attachments: ManiphestSearchAttachments = {} if include_subscribers: attachments["subscribers"] = True if include_projects: attachments["projects"] = True if include_columns: attachments["columns"] = True result = client.maniphest.search_tasks( query_key=query_key or None, constraints=constraints if constraints else None, attachments=attachments if attachments else None, order=order or None, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "results": result} # Diffusion (Repository) Tools @mcp.tool() @handle_api_errors @optimize_token_usage def pha_repository_search( constraints: Dict[str, Any] = None, limit: int = 50, ) -> dict: """ Search for repositories in Phabricator. Args: constraints: Search constraints dictionary (e.g., {"query": "repo_name", "vcs": "git"}) limit: Maximum number of results to return (default: 50, max: 500) Returns: Repository search results with data list and pagination metadata """ client = get_client_func() if constraints is None: constraints = {} result = client.diffusion.search_repositories( constraints=constraints if constraints else None, limit=limit ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "repositories": result} @mcp.tool() @handle_api_errors def pha_repository_create( name: str, vcs_type: str = "git", description: str = "", callsign: str = "", ) -> dict: """ Create a new repository in Phabricator. Args: name: Repository name vcs_type: Version control system type ("git", "hg", "svn") description: Repository description callsign: Optional repository callsign Returns: Created repository information """ client = get_client_func() result = client.diffusion.create_repository( name=name, vcs_type=vcs_type, description=description, callsign=callsign if callsign else None, ) return {"success": True, "repository": result} @mcp.tool() @handle_api_errors def pha_repository_info(repository_identifier: str) -> dict: """ Get detailed information about a specific repository. Args: repository_identifier: Repository ID (numeric or string), PHID, callsign, or name Returns: Repository information """ client = get_client_func() # Try different search strategies based on identifier format result = None # 1. If it looks like a PHID, search by PHID if repository_identifier.startswith("PHID-REPO-"): result = client.diffusion.search_repositories( constraints={"phids": [repository_identifier]}, limit=1, ) # 2. If it's numeric, search by ID elif repository_identifier.isdigit(): result = client.diffusion.search_repositories( constraints={"ids": [int(repository_identifier)]}, limit=1, ) # 3. If it's all uppercase, likely a callsign elif repository_identifier.isupper() and repository_identifier.isalpha(): result = client.diffusion.search_repositories( constraints={"callsigns": [repository_identifier]}, limit=1, ) # 4. Try searching by short name if not result or not result.get("data"): try: result = client.diffusion.search_repositories( constraints={"shortNames": [repository_identifier]}, limit=1, ) except Exception: # shortNames constraint might fail, continue to next strategy pass # 5. If still no results, do a general search and filter by name if not result or not result.get("data"): # Search all repositories and find by name match all_repos = client.diffusion.search_repositories(limit=100) for repo in all_repos.get("data", []): fields = repo.get("fields", {}) if ( fields.get("name") == repository_identifier or fields.get("shortName") == repository_identifier or fields.get("callsign") == repository_identifier ): result = {"data": [repo]} break if result and result.get("data"): return {"success": True, "repository": result["data"][0]} else: return { "success": False, "error": f"Repository '{repository_identifier}' not found", } @mcp.tool() @handle_api_errors @optimize_token_usage def pha_repository_browse( repository: str, path: str = "/", commit: str = "", ) -> dict: """ Browse files and directories in a repository. Args: repository: Repository identifier (PHID, callsign, or name) path: Path to browse (default: root "/") commit: Specific commit to browse (default: latest) Returns: List of files and directories at the specified path with pagination metadata """ client = get_client_func() result = client.diffusion.browse_query( repository=repository, path=path if path else "/", commit=commit if commit else None, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "browse_result": result} @mcp.tool() @handle_api_errors def pha_repository_file_content( repository: str, file_path: str, commit: str = "", ) -> dict: """ Get the content of a specific file from a repository. Args: repository: Repository identifier (PHID, callsign, or name) file_path: Path to the file commit: Specific commit (default: latest) Returns: File content and metadata """ client = get_client_func() result = client.diffusion.file_content_query( repository=repository, path=file_path, commit=commit if commit else None ) return {"success": True, "file_content": result} @mcp.tool() @handle_api_errors @optimize_token_usage def pha_repository_history( repository: str, path: str = "", commit: str = "", limit: int = 20, ) -> dict: """ Get commit history for a repository or specific path. Args: repository: Repository identifier (PHID, callsign, or name) path: Specific path to get history for (optional) commit: Starting commit (default: latest) limit: Maximum number of commits to return (default: 20, max: 100) Returns: Commit history with pagination metadata """ client = get_client_func() result = client.diffusion.history_query( repository=repository, path=path if path else None, commit=commit if commit else None, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "history": result} @mcp.tool() @handle_api_errors def pha_repository_branches(repository: str) -> dict: """ Get all branches in a repository. Args: repository: Repository identifier (PHID, callsign, or name) Returns: List of branches """ client = get_client_func() result = client.diffusion.branch_query(repository=repository) return {"success": True, "branches": result} @mcp.tool() @handle_api_errors def pha_repository_commits_search( repository: str = "", author: str = "", message_contains: str = "", limit: int = 20, ) -> dict: """ Search for commits across repositories. Args: repository: Repository identifier to search in (optional) author: Filter by commit author message_contains: Filter by commit message containing this text limit: Maximum number of results to return Returns: List of matching commits """ client = get_client_func() constraints = {} if repository: constraints["repositories"] = [repository] if author: constraints["authors"] = [author] if message_contains: constraints["query"] = message_contains result = client.diffusion.search_commits( constraints=constraints if constraints else None, limit=limit ) return {"success": True, "commits": result} # Differential (Code Review) Tools @mcp.tool() @handle_api_errors def pha_diff_create_from_content( diff_content: str, repository: str = "", ) -> dict: """ Create a diff from raw diff content. Args: diff_content: Raw unified diff content repository: Repository identifier to associate with (optional) Returns: Created diff information """ client = get_client_func() repository_phid = None if repository: # Try to resolve repository to PHID try: repos = client.diffusion.search_repositories( constraints={"query": repository}, limit=1, ) if repos.get("data"): repository_phid = repos["data"][0]["phid"] except Exception: # If query search fails, try direct PHID if repository.startswith("PHID-"): repository_phid = repository result = client.differential.create_raw_diff( diff=diff_content, repository_phid=repository_phid ) return {"success": True, "diff": result} @mcp.tool() @handle_api_errors def pha_diff_create( diff_id: str, title: str, summary: str = "", test_plan: str = "", reviewers: Optional[List[str]] = None, ) -> dict: """ Create a new code review (Differential revision). Args: diff_id: PHID of the diff to review (use pha_diff_create_from_content to create a diff first) title: Review title summary: Detailed description of the changes test_plan: How the changes were tested reviewers: List of reviewer usernames or PHIDs Returns: Created revision information """ # Initialize None parameters to empty lists if reviewers is None: reviewers = [] client = get_client_func() transactions = [ {"type": "title", "value": title}, {"type": "update", "value": diff_id}, ] if summary: transactions.append({"type": "summary", "value": summary}) if test_plan: transactions.append({"type": "testPlan", "value": test_plan}) if reviewers: transactions.append({"type": "reviewers.add", "value": reviewers}) result = client.differential.edit_revision(transactions=transactions) return {"success": True, "revision": result} @mcp.tool() @handle_api_errors @optimize_token_usage def pha_diff_search( author: str = "", reviewer: str = "", status: str = "", repository: str = "", title_contains: str = "", limit: int = 50, ) -> dict: """ Search for code reviews (Differential revisions). Args: author: Filter by author username or PHID reviewer: Filter by reviewer username or PHID status: Filter by status ("open", "closed", "abandoned", "accepted") repository: Filter by repository PHID (recommended) or name title_contains: Filter by title containing this text limit: Maximum number of results to return (default: 50, max: 500) Returns: List of matching code reviews with pagination metadata """ client = get_client_func() constraints = {} if author: constraints["authorPHIDs"] = [author] if reviewer: constraints["reviewerPHIDs"] = [reviewer] if status: constraints["statuses"] = [status] if repository: constraints["repositoryPHIDs"] = [repository] if title_contains: constraints["query"] = title_contains result = client.differential.search_revisions( constraints=constraints if constraints else None, limit=limit ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "revisions": result} @mcp.tool() @handle_api_errors def pha_diff_get(revision_id: str) -> dict: """ Get detailed information about a specific code review including all diffs. Args: revision_id: Revision ID (e.g., "D123") or PHID Returns: Detailed revision information with all associated diffs """ client = get_client_func() # Parse revision ID if in "D123" format if revision_id.startswith("D"): revision_id = revision_id[1:] result = client.differential.search_revisions( constraints={"ids": [int(revision_id)]}, limit=1 ) if result.get("data"): revision = result["data"][0] # Get all diffs associated with this revision diffs = client.differential.search_diffs( constraints={"revisionPHIDs": [revision["phid"]]}, limit=50, # Allow many diffs for active revisions ) # Add diffs to revision information revision["all_diffs"] = diffs.get("data", []) return {"success": True, "revision": revision} else: return {"success": False, "error": f"Revision {revision_id} not found"} @mcp.tool() @handle_api_errors def pha_diff_add_comment( revision_id: str, comment: str, action: str = "comment", ) -> dict: """ Add a comment to a code review. Args: revision_id: Revision ID (e.g., "D123") or PHID comment: Comment text action: Review action ("comment", "accept", "reject", "request-changes") Returns: Success status """ client = get_client_func() transactions = [{"type": "comment", "value": comment}] if action == "accept": transactions.append({"type": "accept", "value": True}) elif action == "reject": transactions.append({"type": "reject", "value": True}) elif action == "request-changes": transactions.append({"type": "request-changes", "value": True}) client.differential.edit_revision( transactions=transactions, object_identifier=revision_id ) return {"success": True, "comment_added": True} @mcp.tool() @handle_api_errors def pha_diff_update( revision_id: str, new_diff_id: str = "", title: str = "", summary: str = "", test_plan: str = "", comment: str = "", ) -> dict: """ Update an existing code review with new diff or metadata. Args: revision_id: Revision ID (e.g., "D123") or PHID new_diff_id: New diff PHID to update the review with title: New title (optional) summary: New summary (optional) test_plan: New test plan (optional) comment: Comment explaining the update Returns: Updated revision information """ client = get_client_func() transactions = [] if new_diff_id: transactions.append({"type": "update", "value": new_diff_id}) if title: transactions.append({"type": "title", "value": title}) if summary: transactions.append({"type": "summary", "value": summary}) if test_plan: transactions.append({"type": "testPlan", "value": test_plan}) if comment: transactions.append({"type": "comment", "value": comment}) if not transactions: return {"success": False, "error": "No updates specified"} result = client.differential.edit_revision( transactions=transactions, object_identifier=revision_id ) return {"success": True, "revision": result} @mcp.tool() @handle_api_errors def pha_diff_get_content(diff_phid: str) -> dict: """ Get the raw content of a diff using its PHID. Args: diff_phid: Diff PHID in format "PHID-DIFF-xxxxxxxxxxxxxxxxxxxx". Use `pha_diff_get` first to get revision info, then extract the diffPHID from the revision.fields.diffPHID field. Returns: Raw diff content """ client = get_client_func() # Validate PHID format if not diff_phid.startswith("PHID-DIFF-"): return { "success": False, "error": f"Invalid diff PHID format: {diff_phid}. Expected format: PHID-DIFF-xxxxxxxxxxxxxxxxxxxx", "error_code": "INVALID_PHID_FORMAT", } # Search for diff by PHID to get numeric ID diffs = client.differential.search_diffs( constraints={"phids": [diff_phid]}, limit=1 ) if not diffs.get("data"): return { "success": False, "error": f"Diff not found with PHID: {diff_phid}", "error_code": "DIFF_NOT_FOUND", } # Extract numeric ID and get raw diff content diff_data = diffs["data"][0] numeric_diff_id = diff_data["id"] result = client.differential.get_raw_diff(diff_id=numeric_diff_id) return {"success": True, "diff_content": result} @mcp.tool() @handle_api_errors def pha_diff_get_commit_message(revision_id: str) -> dict: """ Get a commit message template for a code review. Args: revision_id: Revision ID (e.g., "D123") or PHID Returns: Formatted commit message template """ client = get_client_func() # Parse revision ID if in "D123" format if revision_id.startswith("D"): revision_id = revision_id[1:] result = client.differential.get_commit_message(revision_id=int(revision_id)) return {"success": True, "commit_message": result} # Project API Tools @mcp.tool() @handle_api_errors @optimize_token_usage def pha_project_search( query_key: str = "", ids: Optional[List[int]] = None, phids: Optional[List[str]] = None, names: Optional[List[str]] = None, name_like: str = "", slugs: Optional[List[str]] = None, ancestors: Optional[List[str]] = None, descendants: Optional[List[str]] = None, depth: int = None, status: str = "", is_milestone: bool = None, has_parent: bool = None, icon: str = "", color: str = "", limit: int = 100, ) -> dict: """ Search for projects with advanced filtering capabilities. Args: query_key: Builtin query ("active", "all", "archived") ids: List of specific project IDs to search for phids: List of specific project PHIDs to search for names: List of exact project names to find name_like: Find projects whose names contain this substring slugs: List of project slugs to find ancestors: Find projects with these ancestors (PHIDs) descendants: Find projects with these descendants (PHIDs) depth: Maximum depth to search for ancestors/descendants status: Filter by project status ("active", "archived") is_milestone: Filter for milestone projects has_parent: Filter for projects with/without parents icon: Filter by project icon color: Filter by project color limit: Maximum number of results to return (default: 100, max: 1000) Returns: Search results with project data and pagination metadata """ # Initialize None parameters to empty lists if ids is None: ids = [] if phids is None: phids = [] if names is None: names = [] if slugs is None: slugs = [] if ancestors is None: ancestors = [] if descendants is None: descendants = [] client = get_client_func() # Build constraints constraints = {} if ids: constraints["ids"] = ids if phids: constraints["phids"] = phids if names: # Use name constraint for exact matches if len(names) == 1: constraints["name"] = names[0] else: # For multiple names, use query to search constraints["query"] = " ".join(names) if name_like: # Use query parameter for substring search (nameLike is not supported) constraints["query"] = name_like if status: constraints["status"] = status # Note: Some constraints like ancestors, descendants, etc. may not be supported # by this Phorge instance. They are included for completeness. result = client.project.search_projects( constraints=constraints if constraints else None, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "projects": result} @mcp.tool() @handle_api_errors def pha_project_create( name: str, description: str = "", icon: str = "", color: str = "", ) -> dict: """ Create a new project in Phabricator. Args: name: Project name (required) description: Project description icon: Project icon (e.g., "fa-briefcase", "fa-users") color: Project color (e.g., "red", "blue", "green") Returns: Created project information """ client = get_client_func() result = client.project.create_project( name=name, description=description, icon=icon if icon else None, color=color if color else None, ) return {"success": True, "project": result} @mcp.tool() @handle_api_errors def pha_project_get(project_identifier: str) -> dict: """ Get detailed information about a specific project. Args: project_identifier: Project ID (e.g., 850), PHID, name, slug, or numeric ID from URL (e.g., extract 850 from https://pha.example.com/project/view/850/) Returns: Project information """ client = get_client_func() # Try different search strategies based on identifier format result = None # 1. If it looks like a PHID, search by PHID if project_identifier.startswith("PHID-PROJ-"): result = client.project.search_projects( constraints={"phids": [project_identifier]}, limit=1, ) # 2. If it's numeric, search by ID elif project_identifier.isdigit(): result = client.project.search_projects( constraints={"ids": [int(project_identifier)]}, limit=1, ) # 3. Try searching by name using name parameter first, then query as fallback if not result or not result.get("data"): # First try exact name match result = client.project.search_projects( constraints={"name": project_identifier}, limit=10, ) # If no results with name, try query if not result.get("data"): result = client.project.search_projects( constraints={"query": project_identifier}, limit=10, ) # Filter for exact match if result.get("data"): exact_match = None for project in result["data"]: fields = project.get("fields", {}) if ( fields.get("name") == project_identifier or fields.get("slug") == project_identifier ): exact_match = project break if exact_match: result = {"data": [exact_match]} if result and result.get("data"): return {"success": True, "project": result["data"][0]} else: return { "success": False, "error": f"Project '{project_identifier}' not found", } @mcp.tool() @handle_api_errors def pha_project_update( project_phid: str, name: str = "", description: str = "", icon: str = "", color: str = "", ) -> dict: """ Update an existing project in Phabricator. Args: project_phid: Project PHID to update name: New project name description: New project description icon: New project icon color: New project color Returns: Updated project information """ client = get_client_func() # Build transactions transactions = [] if name: transactions.append({"type": "name", "value": name}) if description: transactions.append({"type": "description", "value": description}) if icon: transactions.append({"type": "icon", "value": icon}) if color: transactions.append({"type": "color", "value": color}) if not transactions: return {"success": False, "error": "No updates specified"} result = client.project.edit_project( transactions=transactions, object_identifier=project_phid, ) return {"success": True, "project": result} # Workboard Tools @mcp.tool() @handle_api_errors @optimize_token_usage def pha_workboard_search_columns( project_phids: Optional[List[str]] = None, phids: Optional[List[str]] = None, limit: int = 100, ) -> dict: """ Search for workboard columns with filtering capabilities. Args: project_phids: List of project PHIDs to search columns in phids: List of specific column PHIDs to search for limit: Maximum number of results to return (default: 100, max: 1000) Returns: Search results with column data and pagination metadata """ # Initialize None parameters to empty lists if project_phids is None: project_phids = [] if phids is None: phids = [] client = get_client_func() # Build constraints - only use supported parameters constraints = {} if project_phids: constraints["projects"] = project_phids if phids: constraints["phids"] = phids result = client.project.search_columns( constraints=constraints if constraints else None, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "columns": result} @mcp.tool() @handle_api_errors def pha_workboard_move_task( task_id: str, column_phid: str, before_phid: str = "", after_phid: str = "", ) -> dict: """ Move a task to a specific workboard column with optional positioning. Args: task_id: Task ID or PHID to move column_phid: Target column PHID before_phid: Position before this task PHID (optional) after_phid: Position after this task PHID (optional) Returns: Success status and updated task information """ client = get_client_func() # Create column transaction transaction = client.maniphest.create_column_transaction( column_phid=column_phid, before_phid=before_phid or None, after_phid=after_phid or None, ) # Apply the transaction result = client.maniphest.edit_task( object_identifier=task_id, transactions=[transaction], ) return {"success": True, "task": result} @mcp.tool() @handle_api_errors @optimize_token_usage def pha_workboard_search_tasks_by_column( column_phid: str, limit: int = 100, ) -> dict: """ Search for tasks in a specific workboard column. Args: column_phid: Column PHID to search tasks in limit: Maximum number of results to return (default: 100, max: 1000) Returns: Search results with task data and pagination metadata """ client = get_client_func() # Build constraints for column search constraints = { "columnPHIDs": [column_phid], } result = client.maniphest.search_tasks( constraints=constraints, limit=limit, ) # Add pagination metadata result = _add_pagination_metadata(result, result.get("cursor")) return {"success": True, "tasks": result}

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cortex-app/conduit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server