Skip to main content
Glama
saidsef

GitHub PR Issue Analyser

by saidsef

user_activity_query

Query GitHub user activity data across organizations or specific repositories to analyze contributions, commits, pull requests, and issues within defined time periods.

Instructions

Performs a user activity query using GitHub's GraphQL API with support for organization-specific and cross-organization queries.

Query Modes:

  1. Organization-Specific Activity (fastest, most comprehensive):

    • Query organization repositories directly

    • Access all private repos in the org (with proper token scopes)

    • Get detailed commit history, PRs, and issues

    • Variables: {"orgName": "Pelle-Tech", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}

    • Variable types: $orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!

  2. Authenticated User Activity Across All Orgs (slower, summary only):

    • Query viewer's contribution collection

    • Includes all orgs where user is a member

    • Summary counts only (no detailed commit messages)

    • Variables: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}

    • Variable types: $from: DateTime!, $to: DateTime!

  3. User Activity in Specific Organization (most restrictive):

    • Query organization repos filtered by user

    • Requires combining org query with author filtering

    • Variables: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}

    • Variable types: $orgName: String!, $username: String!, $from: GitTimestamp!, $to: GitTimestamp!

Performance Tips:

  • Use pagination parameters to limit initial data: first: 50 instead of first: 100

  • Query only required fields to reduce response size

  • Use org-specific queries when possible (faster than viewer queries)

  • For large date ranges, split into smaller queries

  • Cache results for repeated queries

Example Queries:

Fast Org Query with Pagination:

query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!, $repoCount: Int = 50) {
  organization(login: $orgName) {
    login
    repositories(first: $repoCount, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) {
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        name
        isPrivate
        defaultBranchRef {
          target {
            ... on Commit {
              history(since: $from, until: $to, first: 100) {
                totalCount
                pageInfo {
                  hasNextPage
                  endCursor
                }
                nodes {
                  author { 
                    user { login }
                    email
                  }
                  committedDate
                  message
                  additions
                  deletions
                }
              }
            }
          }
        }
        pullRequests(first: 50, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) {
          totalCount
          nodes {
            number
            title
            author { login }
            createdAt
            state
            additions
            deletions
          }
        }
      }
    }
  }
}

User-Filtered Org Query:

query($orgName: String!, $username: String!, $from: GitTimestamp!, $to: GitTimestamp!) {
  organization(login: $orgName) {
    login
    repositories(first: 100, privacy: PRIVATE) {
      nodes {
        name
        defaultBranchRef {
          target {
            ... on Commit {
              history(since: $from, until: $to, author: {emails: [$username]}, first: 100) {
                totalCount
                nodes {
                  author { user { login } }
                  committedDate
                  message
                }
              }
            }
          }
        }
        pullRequests(first: 100, states: [OPEN, CLOSED, MERGED]) {
          nodes {
            author { login }
            title
            createdAt
            state
          }
        }
      }
    }
  }
}

Cross-Org Viewer Query:

query($from: DateTime!, $to: DateTime!) {
  viewer {
    login
    contributionsCollection(from: $from, to: $to) {
      commitContributionsByRepository(maxRepositories: 100) {
        repository { 
          name 
          isPrivate 
          owner { login }
        }
        contributions { totalCount }
      }
      pullRequestContributionsByRepository(maxRepositories: 100) {
        repository { 
          name 
          isPrivate 
          owner { login }
        }
        contributions { totalCount }
      }
      issueContributionsByRepository(maxRepositories: 100) {
        repository { 
          name 
          isPrivate 
          owner { login }
        }
        contributions { totalCount }
      }
    }
    organizations(first: 100) {
      nodes { 
        login 
        viewerCanAdminister
      }
    }
  }
}

Args: variables (dict[str, Any]): Query variables. Supported combinations: - Org-specific: {"orgName": "Pelle-Tech", "from": "...", "to": "..."} - Cross-org: {"from": "...", "to": "..."} - User-filtered org: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "...", "to": "..."} - With pagination: Add {"repoCount": 50, "prCount": 50} for custom limits query (str): GraphQL query string. Must declare correct variable types: - Organization queries: Use GitTimestamp! for $from/$to - Viewer queries: Use DateTime! for $from/$to - Both types accept ISO 8601 format: "YYYY-MM-DDTHH:MM:SSZ"

Returns: Dict[str, Any]: GraphQL response with activity data or error information. - Success: {"data": {...}} - Errors: {"errors": [...], "data": null} - Network error: {"status": "error", "message": "..."}

Error Handling: - Validates response status codes - Logs GraphQL errors with details - Returns structured error responses - Includes traceback for debugging

Required Token Scopes: - repo: Full control of private repositories - read:org: Read org and team membership - read:user: Read user profile data

Performance Notes: - Org queries are ~3x faster than viewer queries - Large date ranges (>1 year) may timeout - Use pagination for repos with >100 commits - Response size correlates with date range and repo count

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
variablesYes
queryYes

Implementation Reference

  • Core handler implementation for the 'user_activity_query' MCP tool. Executes GitHub GraphQL queries for user activity analysis across repositories, organizations, with pagination support, error handling, and performance optimizations.
    def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str, Any]:
        """
        Performs a user activity query using GitHub's GraphQL API with support for organization-specific 
        and cross-organization queries.
    
        **Query Modes**:
        
        1. **Organization-Specific Activity** (fastest, most comprehensive):
           - Query organization repositories directly
           - Access all private repos in the org (with proper token scopes)
           - Get detailed commit history, PRs, and issues
           - Variables: {"orgName": "Pelle-Tech", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
           - Variable types: `$orgName: String!`, `$from: GitTimestamp!`, `$to: GitTimestamp!`
        
        2. **Authenticated User Activity Across All Orgs** (slower, summary only):
           - Query viewer's contribution collection
           - Includes all orgs where user is a member
           - Summary counts only (no detailed commit messages)
           - Variables: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
           - Variable types: `$from: DateTime!`, `$to: DateTime!`
        
        3. **User Activity in Specific Organization** (most restrictive):
           - Query organization repos filtered by user
           - Requires combining org query with author filtering
           - Variables: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
           - Variable types: `$orgName: String!`, `$username: String!`, `$from: GitTimestamp!`, `$to: GitTimestamp!`
    
        **Performance Tips**:
        - Use pagination parameters to limit initial data: `first: 50` instead of `first: 100`
        - Query only required fields to reduce response size
        - Use org-specific queries when possible (faster than viewer queries)
        - For large date ranges, split into smaller queries
        - Cache results for repeated queries
    
        **Example Queries**:
    
        **Fast Org Query with Pagination**:
        ```graphql
        query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!, $repoCount: Int = 50) {
          organization(login: $orgName) {
            login
            repositories(first: $repoCount, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) {
              pageInfo {
                hasNextPage
                endCursor
              }
              nodes {
                name
                isPrivate
                defaultBranchRef {
                  target {
                    ... on Commit {
                      history(since: $from, until: $to, first: 100) {
                        totalCount
                        pageInfo {
                          hasNextPage
                          endCursor
                        }
                        nodes {
                          author { 
                            user { login }
                            email
                          }
                          committedDate
                          message
                          additions
                          deletions
                        }
                      }
                    }
                  }
                }
                pullRequests(first: 50, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) {
                  totalCount
                  nodes {
                    number
                    title
                    author { login }
                    createdAt
                    state
                    additions
                    deletions
                  }
                }
              }
            }
          }
        }
        ```
    
        **User-Filtered Org Query**:
        ```graphql
        query($orgName: String!, $username: String!, $from: GitTimestamp!, $to: GitTimestamp!) {
          organization(login: $orgName) {
            login
            repositories(first: 100, privacy: PRIVATE) {
              nodes {
                name
                defaultBranchRef {
                  target {
                    ... on Commit {
                      history(since: $from, until: $to, author: {emails: [$username]}, first: 100) {
                        totalCount
                        nodes {
                          author { user { login } }
                          committedDate
                          message
                        }
                      }
                    }
                  }
                }
                pullRequests(first: 100, states: [OPEN, CLOSED, MERGED]) {
                  nodes {
                    author { login }
                    title
                    createdAt
                    state
                  }
                }
              }
            }
          }
        }
        ```
    
        **Cross-Org Viewer Query**:
        ```graphql
        query($from: DateTime!, $to: DateTime!) {
          viewer {
            login
            contributionsCollection(from: $from, to: $to) {
              commitContributionsByRepository(maxRepositories: 100) {
                repository { 
                  name 
                  isPrivate 
                  owner { login }
                }
                contributions { totalCount }
              }
              pullRequestContributionsByRepository(maxRepositories: 100) {
                repository { 
                  name 
                  isPrivate 
                  owner { login }
                }
                contributions { totalCount }
              }
              issueContributionsByRepository(maxRepositories: 100) {
                repository { 
                  name 
                  isPrivate 
                  owner { login }
                }
                contributions { totalCount }
              }
            }
            organizations(first: 100) {
              nodes { 
                login 
                viewerCanAdminister
              }
            }
          }
        }
        ```
    
        Args:
            variables (dict[str, Any]): Query variables. Supported combinations:
                - Org-specific: {"orgName": "Pelle-Tech", "from": "...", "to": "..."}
                - Cross-org: {"from": "...", "to": "..."}
                - User-filtered org: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "...", "to": "..."}
                - With pagination: Add {"repoCount": 50, "prCount": 50} for custom limits
            query (str): GraphQL query string. Must declare correct variable types:
                - Organization queries: Use `GitTimestamp!` for $from/$to
                - Viewer queries: Use `DateTime!` for $from/$to
                - Both types accept ISO 8601 format: "YYYY-MM-DDTHH:MM:SSZ"
    
        Returns:
            Dict[str, Any]: GraphQL response with activity data or error information.
                - Success: {"data": {...}}
                - Errors: {"errors": [...], "data": null}
                - Network error: {"status": "error", "message": "..."}
    
        Error Handling:
            - Validates response status codes
            - Logs GraphQL errors with details
            - Returns structured error responses
            - Includes traceback for debugging
    
        Required Token Scopes:
            - `repo`: Full control of private repositories
            - `read:org`: Read org and team membership
            - `read:user`: Read user profile data
    
        Performance Notes:
            - Org queries are ~3x faster than viewer queries
            - Large date ranges (>1 year) may timeout
            - Use pagination for repos with >100 commits
            - Response size correlates with date range and repo count
        """
        # Validate inputs
        if not query or not isinstance(query, str):
            return {"status": "error", "message": "Query must be a non-empty string"}
        
        if not variables or not isinstance(variables, dict):
            return {"status": "error", "message": "Variables must be a non-empty dictionary"}
        
        # Determine query type for optimized logging
        query_type = "unknown"
        if "orgName" in variables and "username" in variables:
            query_type = "user-filtered-org"
        elif "orgName" in variables:
            query_type = "org-specific"
        elif "from" in variables and "to" in variables:
            query_type = "cross-org-viewer"
        
        logging.info(f"Performing GraphQL query [type: {query_type}] with variables: {variables}")
    
        try:
            # Make GraphQL request with optimized timeout
            response = requests.post(
                'https://api.github.com/graphql',
                json={'query': query, 'variables': variables},
                headers=self._get_headers(),
                timeout=TIMEOUT * 2  # Double timeout for GraphQL queries (can be complex)
            )
            response.raise_for_status()
            query_data = response.json()
    
            # Handle GraphQL errors (API accepts request but query has issues)
            if 'errors' in query_data:
                error_messages = [err.get('message', 'Unknown error') for err in query_data['errors']]
                logging.error(f"GraphQL query errors: {error_messages}")
                
                # Check for common errors and provide helpful messages
                for error in query_data['errors']:
                    error_type = error.get('extensions', {}).get('code')
                    if error_type == 'variableMismatch':
                        logging.error(f"Variable type mismatch: Use GitTimestamp for org queries, DateTime for viewer queries")
                    elif error_type == 'NOT_FOUND':
                        logging.error(f"Resource not found: Check org/user name is correct and case-sensitive")
                    elif error_type == 'FORBIDDEN':
                        logging.error(f"Access forbidden: Check token has required scopes (repo, read:org)")
                
                return query_data  # Return with errors for caller to handle
            
            # Log success with summary
            if 'data' in query_data:
                data_keys = list(query_data['data'].keys())
                logging.info(f"GraphQL query successful [type: {query_type}], returned data keys: {data_keys}")
            
            return query_data
    
        except requests.exceptions.Timeout:
            error_msg = f"GraphQL query timeout after {TIMEOUT * 2}s. Try reducing date range or repo count."
            logging.error(error_msg)
            return {"status": "error", "message": error_msg, "timeout": True}
        
        except requests.exceptions.RequestException as req_err:
            error_msg = f"Request error during GraphQL query: {str(req_err)}"
            logging.error(error_msg)
            traceback.print_exc()
            return {"status": "error", "message": error_msg, "request_exception": True}
        
        except Exception as e:
            error_msg = f"Unexpected error performing GraphQL query: {str(e)}"
            logging.error(error_msg)
            traceback.print_exc()
            return {"status": "error", "message": error_msg, "unexpected": True}
  • Dynamic registration of all public methods from GitHubIntegration instance (including 'user_activity_query') as MCP tools via FastMCP.add_tool()
    def register_tools(self, methods: Any = None) -> None:
        for name, method in inspect.getmembers(methods):
            if (inspect.isfunction(method) or inspect.ismethod(method)) and not name.startswith("_"):
                self.mcp.add_tool(method)
  • Calls register_tools on GitHubIntegration instance to register 'user_activity_query' and other tools.
    def _register_tools(self):
        self.register_tools(self.gi)
        self.register_tools(self.ip)
  • Higher-level helper method 'get_user_org_activity' that internally uses 'user_activity_query' to fetch comprehensive, paginated user activity across all repositories in an organization.
    def get_user_org_activity(
        self, 
        org_name: str, 
        username: str, 
        from_date: str, 
        to_date: str,
        page: int = 1,
        per_page: int = 50
    ) -> Dict[str, Any]:
        """
        Gets comprehensive activity for a SPECIFIC USER across ALL repositories in an organization.
        
        **PAGINATED RESULTS** - Returns a manageable subset of data to prevent context overflow.
        
        Efficiently filters by user at the GraphQL level - does NOT scan entire repos.
        Captures ALL branches, not just main/default branch.
        
        Includes:
        - Commits by the user (paginated)
        - PRs where user was: author, reviewer, merger, commenter, or assigned (paginated)
        - Issues where user was: author, assigned, commenter, or participant (paginated)
        - Handles reviewed, open, merged, closed, and approved PRs
        
        Args:
            org_name (str): GitHub organization name
            username (str): GitHub username to query
            from_date (str): Start date ISO 8601 (e.g., "2024-01-01T00:00:00Z")
            to_date (str): End date ISO 8601 (e.g., "2024-12-31T23:59:59Z")
            page (int): Page number (1-indexed, default: 1)
            per_page (int): Items per page (default: 50, max: 100)
        
        Returns:
            Dict containing: 
            - status: success/error
            - summary: aggregate statistics
            - commits[]: paginated commits (most recent first)
            - prs[]: paginated PRs (most recent first)
            - issues[]: paginated issues (most recent first)
            - pagination: current_page, per_page, total_items, total_pages, has_next_page
        """
        logging.info(f"Fetching ALL activity for '{username}' in '{org_name}' from {from_date} to {to_date}")
        
        # Step 1: Get user's email addresses for efficient commit filtering
        user_emails = self._get_user_emails(username)
        logging.info(f"Found {len(user_emails)} email(s) for filtering commits")
        
        # Step 2: Get repositories where user actually contributed (optimized approach)
        # First try to get repos from user's contribution collection
        contributed_repos = self._get_user_contributed_repos(username, org_name, from_date, to_date)
        
        if contributed_repos:
            logging.info(f"Found {len(contributed_repos)} repos with user contributions via contributionsCollection")
            org_repos = contributed_repos
        else:
            # Fallback: Get ALL repositories in organization
            logging.info(f"Fallback: Scanning all org repos (contributionsCollection returned no results)")
            org_repos = self._get_all_org_repos(org_name)
            logging.info(f"Found {len(org_repos)} total repositories in {org_name}")
        
        if not org_repos:
            return self._empty_activity_response(username, org_name, from_date, to_date, page, per_page)
        
        # Step 3: Process each repo - filter by user at GraphQL level
        all_commits = []
        all_prs = []
        all_issues = []
        repos_with_activity = 0
        
        for repo_info in org_repos:
            repo_name = repo_info.get("name")
            repo_url = repo_info.get("url")
            
            logging.info(f"Scanning {org_name}/{repo_name} for {username}")
            
            # Fetch user-specific data from this repo
            repo_activity = self._fetch_repo_user_activity(
                org_name, repo_name, repo_url, username, user_emails, from_date, to_date
            )
            
            if repo_activity:
                all_commits.extend(repo_activity.get("commits", []))
                all_prs.extend(repo_activity.get("prs", []))
                all_issues.extend(repo_activity.get("issues", []))
                
                if repo_activity.get("has_activity"):
                    repos_with_activity += 1
        
        # Sort by date (most recent first)
        all_commits.sort(key=lambda x: x["date"], reverse=True)
        all_prs.sort(key=lambda x: x["updated_at"], reverse=True)
        all_issues.sort(key=lambda x: x["updated_at"], reverse=True)
        
        # Calculate pagination
        per_page = min(max(1, per_page), 100)  # Clamp between 1-100
        page = max(1, page)  # Must be at least 1
        
        total_commits = len(all_commits)
        total_prs = len(all_prs)
        total_issues = len(all_issues)
        
        # Calculate pages
        commits_total_pages = (total_commits + per_page - 1) // per_page if total_commits > 0 else 1
        prs_total_pages = (total_prs + per_page - 1) // per_page if total_prs > 0 else 1
        issues_total_pages = (total_issues + per_page - 1) // per_page if total_issues > 0 else 1
        
        # Slice data for current page
        start_idx = (page - 1) * per_page
        end_idx = start_idx + per_page
        
        paginated_commits = all_commits[start_idx:end_idx]
        paginated_prs = all_prs[start_idx:end_idx]
        paginated_issues = all_issues[start_idx:end_idx]
        
        # Generate summary (based on ALL data, not just current page)
        user_authored_prs = [pr for pr in all_prs if "Author" in pr["user_roles"]]
        
        summary = {
            "user": username,
            "organization": org_name,
            "date_range": f"{from_date} to {to_date}",
            "total_commits": total_commits,
            "total_prs_involved": total_prs,
            "prs_authored": len(user_authored_prs),
            "prs_reviewed": len([pr for pr in all_prs if any(r in pr["user_roles"] for r in ["Approved", "Requested Changes", "Reviewed"])]),
            "prs_merged": len([pr for pr in all_prs if "Merged" in pr["user_roles"]]),
            "prs_commented": len([pr for pr in all_prs if "Commented" in pr["user_roles"]]),
            "total_issues_involved": len(all_issues),
            "issues_authored": len([issue for issue in all_issues if "Author" in issue["user_roles"]]),
            "issues_assigned": len([issue for issue in all_issues if "Assigned" in issue["user_roles"]]),
            "issues_commented": len([issue for issue in all_issues if "Commented" in issue["user_roles"]]),
            "total_additions": sum(c["additions"] for c in all_commits),
            "total_deletions": sum(c["deletions"] for c in all_commits),
        }
        
        logging.info(f"Activity complete: Page {page}/{max(commits_total_pages, prs_total_pages, issues_total_pages)} - Returning {len(paginated_commits)} commits, {len(paginated_prs)} PRs, {len(paginated_issues)} issues from {repos_with_activity}/{len(org_repos)} repos")
        
        return {
            "status": "success",
            "summary": summary,
            "commits": paginated_commits,
            "prs": paginated_prs,
            "issues": paginated_issues,
            "pagination": {
                "current_page": page,
                "per_page": per_page,
                "commits": {
                    "total": total_commits,
                    "total_pages": commits_total_pages,
                    "has_next_page": page < commits_total_pages,
                    "returned": len(paginated_commits)
                },
                "prs": {
                    "total": total_prs,
                    "total_pages": prs_total_pages,
                    "has_next_page": page < prs_total_pages,
                    "returned": len(paginated_prs)
                },
                "issues": {
                    "total": total_issues,
                    "total_pages": issues_total_pages,
                    "has_next_page": page < issues_total_pages,
                    "returned": len(paginated_issues)
                },
                "repos": {
                    "total_in_org": len(org_repos),
                    "with_user_activity": repos_with_activity
                }
            }
        }

Latest Blog Posts

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/saidsef/mcp-github-pr-issue-analyser'

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