Skip to main content
Glama

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