From 3552bb86ab54bfcf1551604d2ba96398449d3260 Mon Sep 17 00:00:00 2001 From: Said Sef Date: Mon, 17 Nov 2025 00:28:31 +0000 Subject: [PATCH 1/5] chore: ci workflow trigger From f05cf04f96622f690f7d387cf07ce2b0896f822a Mon Sep 17 00:00:00 2001 From: Said Sef Date: Mon, 17 Nov 2025 14:16:54 +0000 Subject: [PATCH 2/5] feat(github): add user activity query method using GitHub GraphQL API --- src/mcp_github/github_integration.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/mcp_github/github_integration.py b/src/mcp_github/github_integration.py index 09fbc7c..9578bcf 100644 --- a/src/mcp_github/github_integration.py +++ b/src/mcp_github/github_integration.py @@ -665,3 +665,28 @@ def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release logging.error(f"Error creating release: {str(e)}") traceback.print_exc() return {"status": "error", "message": str(e)} + + def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str, Any]: + """ + Performs a user activity query using GitHub's GraphQL API across all repositories and organisations. + Args: + variables (dict[str, Any]): The variables to include in the query i.e. {"login": "username", "from": "2023-01-01", "to": "2023-12-31"}. + query (str): The search query string. GitHub GraphQL query summary collection that includes all activity across all orgs, public and private. + Returns: + Dict[str, Any]: The JSON response from the GitHub API containing search results if successful. + """ + logging.info("Performing user query on GitHub") + + try: + response = requests.post('https://api.github.com/graphql', json={'query': query, 'variables': variables}, headers=self._get_headers(), timeout=TIMEOUT) + response.raise_for_status() + query_data = response.json() + return query_data + except requests.exceptions.RequestException as req_err: + logging.error(f"Request error during user activity query: {str(req_err)}") + traceback.print_exc() + return {"status": "error", "message": str(req_err)} + except Exception as e: + logging.error(f"Error performing user activity query: {str(e)}") + traceback.print_exc() + return {"status": "error", "message": str(e)} From ce79d53af66cff7cc7cbaf1001d3c787dd61c95f Mon Sep 17 00:00:00 2001 From: Said Sef Date: Mon, 17 Nov 2025 14:28:51 +0000 Subject: [PATCH 3/5] fix(github): e501 line too long --- src/mcp_github/github_integration.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mcp_github/github_integration.py b/src/mcp_github/github_integration.py index 9578bcf..f0dc356 100644 --- a/src/mcp_github/github_integration.py +++ b/src/mcp_github/github_integration.py @@ -678,7 +678,12 @@ def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str logging.info("Performing user query on GitHub") try: - response = requests.post('https://api.github.com/graphql', json={'query': query, 'variables': variables}, headers=self._get_headers(), timeout=TIMEOUT) + response = requests.post( + 'https://api.github.com/graphql', + json={'query': query, 'variables': variables}, + headers=self._get_headers(), + timeout=TIMEOUT + ) response.raise_for_status() query_data = response.json() return query_data From 16cd4609f377c87559424106af5455b7450b3f0e Mon Sep 17 00:00:00 2001 From: Said Sef Date: Mon, 17 Nov 2025 19:49:36 +0000 Subject: [PATCH 4/5] feat(github): enhance user activity query documentation for authenticated user access --- src/mcp_github/github_integration.py | 99 ++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/src/mcp_github/github_integration.py b/src/mcp_github/github_integration.py index f0dc356..69e6813 100644 --- a/src/mcp_github/github_integration.py +++ b/src/mcp_github/github_integration.py @@ -668,14 +668,99 @@ def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str, Any]: """ - Performs a user activity query using GitHub's GraphQL API across all repositories and organisations. + Performs a user activity query using GitHub's GraphQL API for the authenticated user (token owner). + + **Critical**: To query activities within a specific organization (e.g., "saidsef"): + 1. Query the organization DIRECTLY using `organization(login: "saidsef")` + 2. Do NOT use `viewer.contributionsCollection` as it excludes many private org activities + 3. Organization name is CASE-SENSITIVE - must match exactly + + **For Organization-Specific Activity** (e.g., saidsef): + ```graphql + query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!) { + organization(login: $orgName) { + login + repositories(first: 100, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) { + nodes { + name + isPrivate + owner { login } + defaultBranchRef { + target { + ... on Commit { + history(since: $from, until: $to) { + totalCount + nodes { + author { user { login } } + committedDate + message + } + } + } + } + } + pullRequests(first: 100, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + number + title + author { login } + createdAt + state + } + } + issues(first: 100, states: [OPEN, CLOSED], orderBy: {field: UPDATED_AT, direction: DESC}) { + nodes { + number + title + author { login } + createdAt + state + } + } + } + } + } + } + ``` + + **For Authenticated User Activity Across All Orgs**: + ```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 } + } + } + organizations(first: 100) { + nodes { login } + } + } + } + ``` + Args: - variables (dict[str, Any]): The variables to include in the query i.e. {"login": "username", "from": "2023-01-01", "to": "2023-12-31"}. - query (str): The search query string. GitHub GraphQL query summary collection that includes all activity across all orgs, public and private. + variables (dict[str, Any]): Query variables. Options: + - For org-specific with commit history: {"orgName": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"} + Note: Use GitTimestamp type (ISO 8601 format) for $from/$to when querying commit history + - For all activity: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"} + Note: Use DateTime type for contributionsCollection queries + query (str): GraphQL query string. Use `organization(login: $orgName)` for specific org queries. + IMPORTANT: Declare variables as `GitTimestamp!` for commit history, `DateTime!` for contributionsCollection. + Returns: - Dict[str, Any]: The JSON response from the GitHub API containing search results if successful. + Dict[str, Any]: The JSON response from GitHub's GraphQL API containing activity data. + + Note: Organization queries require `read:org` scope. Organization name is case-sensitive. + GitTimestamp and DateTime both accept ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). """ - logging.info("Performing user query on GitHub") + logging.info(f"Performing GraphQL query with variables: {variables}") try: response = requests.post( @@ -686,6 +771,10 @@ def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str ) response.raise_for_status() query_data = response.json() + + if 'errors' in query_data: + logging.error(f"GraphQL errors: {query_data['errors']}") + return query_data except requests.exceptions.RequestException as req_err: logging.error(f"Request error during user activity query: {str(req_err)}") From 8e4d7670fe19c4dcdc0b4abf9c7fc9c1ce8f288a Mon Sep 17 00:00:00 2001 From: Said Sef Date: Fri, 21 Nov 2025 19:16:22 +0000 Subject: [PATCH 5/5] feat(github): enhance user activity query with organization-specific and cross-org support --- src/mcp_github/github_integration.py | 230 ++++++++++++++++++++++----- 1 file changed, 191 insertions(+), 39 deletions(-) diff --git a/src/mcp_github/github_integration.py b/src/mcp_github/github_integration.py index 69e6813..223a567 100644 --- a/src/mcp_github/github_integration.py +++ b/src/mcp_github/github_integration.py @@ -668,51 +668,120 @@ def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str, Any]: """ - Performs a user activity query using GitHub's GraphQL API for the authenticated user (token owner). + Performs a user activity query using GitHub's GraphQL API with support for organization-specific + and cross-organization queries. - **Critical**: To query activities within a specific organization (e.g., "saidsef"): - 1. Query the organization DIRECTLY using `organization(login: "saidsef")` - 2. Do NOT use `viewer.contributionsCollection` as it excludes many private org activities - 3. Organization name is CASE-SENSITIVE - must match exactly - - **For Organization-Specific Activity** (e.g., saidsef): + **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!) { + query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!, $repoCount: Int = 50) { organization(login: $orgName) { login - repositories(first: 100, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) { + repositories(first: $repoCount, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) { + pageInfo { + hasNextPage + endCursor + } nodes { name isPrivate - owner { login } defaultBranchRef { target { ... on Commit { - history(since: $from, until: $to) { + history(since: $from, until: $to, first: 100) { totalCount + pageInfo { + hasNextPage + endCursor + } nodes { - author { user { login } } + author { + user { login } + email + } committedDate message + additions + deletions } } } } } - pullRequests(first: 100, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) { + 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 + } + } + } } } - issues(first: 100, states: [OPEN, CLOSED], orderBy: {field: UPDATED_AT, direction: DESC}) { + pullRequests(first: 100, states: [OPEN, CLOSED, MERGED]) { nodes { - number - title author { login } + title createdAt state } @@ -723,64 +792,147 @@ def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str } ``` - **For Authenticated User Activity Across All Orgs**: + **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 } } + repository { + name + isPrivate + owner { login } + } contributions { totalCount } } pullRequestContributionsByRepository(maxRepositories: 100) { - repository { name isPrivate owner { login } } + repository { + name + isPrivate + owner { login } + } + contributions { totalCount } + } + issueContributionsByRepository(maxRepositories: 100) { + repository { + name + isPrivate + owner { login } + } contributions { totalCount } } } organizations(first: 100) { - nodes { login } + nodes { + login + viewerCanAdminister + } } } } ``` Args: - variables (dict[str, Any]): Query variables. Options: - - For org-specific with commit history: {"orgName": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"} - Note: Use GitTimestamp type (ISO 8601 format) for $from/$to when querying commit history - - For all activity: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"} - Note: Use DateTime type for contributionsCollection queries - query (str): GraphQL query string. Use `organization(login: $orgName)` for specific org queries. - IMPORTANT: Declare variables as `GitTimestamp!` for commit history, `DateTime!` for contributionsCollection. + 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]: The JSON response from GitHub's GraphQL API containing activity data. + Dict[str, Any]: GraphQL response with activity data or error information. + - Success: {"data": {...}} + - Errors: {"errors": [...], "data": null} + - Network error: {"status": "error", "message": "..."} - Note: Organization queries require `read:org` scope. Organization name is case-sensitive. - GitTimestamp and DateTime both accept ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ). - """ - logging.info(f"Performing GraphQL query with variables: {variables}") + 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 + 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: - logging.error(f"GraphQL errors: {query_data['errors']}") - + 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: - logging.error(f"Request error during user activity query: {str(req_err)}") + error_msg = f"Request error during GraphQL query: {str(req_err)}" + logging.error(error_msg) traceback.print_exc() - return {"status": "error", "message": str(req_err)} + return {"status": "error", "message": error_msg, "request_exception": True} + except Exception as e: - logging.error(f"Error performing user activity query: {str(e)}") + error_msg = f"Unexpected error performing GraphQL query: {str(e)}" + logging.error(error_msg) traceback.print_exc() - return {"status": "error", "message": str(e)} + return {"status": "error", "message": error_msg, "unexpected": True}