Skip to main content
Glama
radireddy

GitHub MCP Server

by radireddy

github.getUserComments

Fetch and analyze all GitHub comments by a specific user in a repository within a time range, combining review and issue comments into a unified format for activity tracking and feedback analysis.

Instructions

Fetch all comments (review comments and issue comments) added by a user for a given repository within a time duration. Combines PR review comments (inline comments from PR reviews) and PR issue comments (general comments on PRs), normalizes them into a unified format, and deduplicates by comment ID. Filters by comment.createdAt and author. Automatically filters out auto-generated comments and comments on auto-created PRs. Use this tool to get a complete view of all user comments in a repository.

Example use cases:

  • Get all comments by a user in a repository for analysis

  • Track comment activity and engagement

  • Analyze comment patterns and types

  • Extract all user feedback for sentiment analysis

Returns: Array of comment objects with id, body, createdAt, author, prId, prNumber, prTitle, prRepo, commentType (review/issue), filePath, lineNumber, reviewId

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
usernameYesGitHub username (case-insensitive, @ prefix optional). Examples: "octocat", "@octocat"
repoYesRepository in owner/repo format. Required - only comments for PRs in this repository will be returned. Example: "owner/repo"
fromYesStart timestamp in ISO 8601 format. Example: "2024-01-01T00:00:00Z"
toYesEnd timestamp in ISO 8601 format. Example: "2024-12-31T23:59:59Z"

Implementation Reference

  • The core handler method in GitHubTools class that implements github.getUserComments tool. Fetches both PR review comments (using efficient contributionsCollection) and PR issue comments, applies filters (auto-generated content, repositories, time range), normalizes to UserComment format, deduplicates by ID, and sorts by creation date.
    async getUserComments(
      username: string,
      repos: string[],
      from?: string,
      to?: string
    ): Promise<{ comments: UserComment[] }> {
      const { normalizedUsername, normalizedRepos, from: validatedFrom, to: validatedTo } =
        this.validateCommonParameters(username, repos, from, to);
    
      const fromTime = new Date(validatedFrom).getTime();
      const toTime = new Date(validatedTo).getTime();
    
      const allComments: UserComment[] = [];
    
      // OPTIMIZED APPROACH: Use contributionsCollection for review comments (much more efficient)
      // This only fetches PRs where the user actually commented, not all PRs in the repo
      console.error(`[getUserComments] Step 1/2: Fetching PR review comments using contributionsCollection (efficient)...`);
      const reviewCommentsFromContributions = await fetchAllPages(
        async (cursor: string | null) => {
          // Check rate limit before making call
          try {
            const response = await this.client.query(QUERIES.ReviewComments, {
              username: normalizedUsername,
              from: validateTimestamp(validatedFrom),
              to: validateTimestamp(validatedTo),
              after: cursor,
            });
    
            // Check rate limit after call
            const rateLimit = extractRateLimit(response.headers || {});
            if (rateLimit && rateLimit.remaining < 50) {
              console.error(`[getUserComments] Rate limit warning: ${rateLimit.remaining} requests remaining`);
            }
    
            return response.data;
          } catch (error: any) {
            if (error.message?.includes('rate limit')) {
              console.error(`[getUserComments] Rate limit exceeded, stopping to avoid further errors`);
              throw error;
            }
            throw error;
          }
        },
        (data: any) => {
          const contributions = data.user?.contributionsCollection
            ?.pullRequestReviewContributions?.nodes || [];
          const comments: UserComment[] = [];
    
          for (const contribution of contributions) {
            const review = contribution.pullRequestReview;
            const pr = review.pullRequest;
    
            // Filter by repository (must match one of the provided repos)
            const prRepo = pr.repository?.nameWithOwner?.toLowerCase();
            if (!prRepo || !normalizedRepos.includes(prRepo)) {
              continue;
            }
    
            // Filter out auto-created PRs
            if (this.isAutoCreatedPR(pr.title)) {
              continue;
            }
    
            // Extract review comments
            const reviewComments = mapReviewComments(contribution);
            for (const comment of reviewComments) {
              // Filter by time (contributionsCollection already filters, but double-check)
              const commentTime = new Date(comment.timestamp).getTime();
              if (commentTime >= fromTime && commentTime <= toTime) {
                // Use shared filter method
                if (!this.filterReviewComment(comment, normalizedRepos)) {
                  continue;
                }
    
                // Convert ReviewComment to UserComment
                comments.push({
                  id: comment.id,
                  body: comment.body,
                  createdAt: comment.timestamp,
                  author: normalizedUsername,
                  prId: comment.prId,
                  prNumber: comment.prNumber,
                  prTitle: comment.prTitle,
                  prRepo: comment.prRepo,
                  commentType: 'review',
                  filePath: comment.filePath,
                  lineNumber: comment.lineNumber,
                  reviewId: comment.reviewId,
                });
              }
            }
          }
    
          return comments;
        },
        (data: any) => {
          const pageInfo = data.user?.contributionsCollection
            ?.pullRequestReviewContributions?.pageInfo || {};
          return extractPageInfo(pageInfo);
        }
      );
    
      console.error(`[getUserComments] Found ${reviewCommentsFromContributions.length} review comments from contributionsCollection`);
      allComments.push(...reviewCommentsFromContributions);
    
      // Step 2: Fetch PR issue comments
      // For issue comments, we still need to fetch PRs, but we can optimize by:
      // 1. Using search to find PRs commented on by the user (if possible)
      // 2. Limiting the number of PRs processed
      // 3. Adding rate limit checks
    
      // For now, use a more limited approach: only fetch recent PRs or use search
      // Since GitHub search doesn't support commenter filter well, we'll use a hybrid:
      // Try to get issue comments from PRs we already know about (from review comments)
      const prIdsFromReviewComments = new Set(
        reviewCommentsFromContributions.map(c => c.prId)
      );
    
      console.error(`[getUserComments] Step 2/2: Fetching PR issue comments from ${prIdsFromReviewComments.size} known PRs...`);
    
      if (prIdsFromReviewComments.size > 0) {
        // Fetch issue comments only from PRs we know the user interacted with
        const prIdsArray = Array.from(prIdsFromReviewComments);
        const batchSize = 100;
    
        for (let i = 0; i < prIdsArray.length; i += batchSize) {
          const batch = prIdsArray.slice(i, i + batchSize);
          try {
            const response = await this.client.query(QUERIES.PRIssueCommentsByPRIds, {
              prIds: batch,
            });
    
            const prs = response.data.nodes || [];
            for (const pr of prs) {
              if (!pr || !pr.id) continue;
    
              // Filter out auto-created PRs
              if (this.isAutoCreatedPR(pr.title)) {
                continue;
              }
    
              const issueComments = pr.comments?.nodes || [];
              for (const comment of issueComments) {
                if (!comment) continue;
    
                const commentAuthor = comment.author?.login?.toLowerCase();
                const commentTime = new Date(comment.createdAt).getTime();
    
                if (
                  commentAuthor === normalizedUsername &&
                  commentTime >= fromTime &&
                  commentTime <= toTime
                ) {
                  // Use shared filter method (checking auto-generated comments)
                  if (this.isAutoGeneratedComment(comment.body)) {
                    continue;
                  }
    
                  allComments.push(mapIssueCommentToUserComment(comment, pr));
                }
              }
            }
          } catch (error: any) {
            console.error(`[getUserComments] Error fetching issue comments for batch: ${error.message}`);
            // Continue with other batches
          }
        }
      } else {
        // Fallback: If no review comments, we still need to get issue comments
        // But limit to a reasonable number of PRs to avoid rate limits
        const MAX_PRS_TO_PROCESS = 500; // Limit to prevent rate limit issues
    
        console.error(`[getUserComments] No review comments found, fetching issue comments for ${normalizedRepos.length} repo(s) (limited to ${MAX_PRS_TO_PROCESS} PRs per repo)...`);
    
        // Process each repo separately
        for (const repo of normalizedRepos) {
          const { owner, name: repoName } = this.validateAndParseRepo(repo);
          let pageCount = 0;
          let totalPRsProcessed = 0;
    
          const issueComments = await fetchAllPages(
            async (cursor: string | null) => {
              // Check if we've processed too many PRs
              if (totalPRsProcessed >= MAX_PRS_TO_PROCESS) {
                console.error(`[getUserComments] Reached limit of ${MAX_PRS_TO_PROCESS} PRs for ${repo}, stopping to avoid rate limits`);
                return { repository: { pullRequests: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } };
              }
    
              pageCount++;
              if (pageCount % 10 === 0) {
                console.error(`[getUserComments] Processing page ${pageCount} of PRs (issue comments) for ${repo}...`);
              }
              const response = await this.client.query(QUERIES.PRIssueCommentsByRepo, {
                owner,
                repo: repoName,
                cursor,
              });
              return response.data;
            },
            (data: any) => {
              const prs = data.repository?.pullRequests?.nodes || [];
              totalPRsProcessed += prs.length;
              const comments: UserComment[] = [];
    
              for (const pr of prs) {
                if (!pr || !pr.id) continue;
    
                // Filter out auto-created PRs
                if (this.isAutoCreatedPR(pr.title)) {
                  continue;
                }
    
                const issueComments = pr.comments?.nodes || [];
                for (const comment of issueComments) {
                  if (!comment) continue;
    
                  const commentAuthor = comment.author?.login?.toLowerCase();
                  const commentTime = new Date(comment.createdAt).getTime();
    
                  if (
                    commentAuthor === normalizedUsername &&
                    commentTime >= fromTime &&
                    commentTime <= toTime
                  ) {
                    // Filter out auto-generated comments
                    if (this.isAutoGeneratedComment(comment.body)) {
                      continue;
                    }
    
                    comments.push(mapIssueCommentToUserComment(comment, pr));
                  }
                }
              }
    
              return comments;
            },
            (data: any) => {
              const pageInfo = data.repository?.pullRequests?.pageInfo || {};
              // Stop if we've processed too many PRs
              if (totalPRsProcessed >= MAX_PRS_TO_PROCESS) {
                return { hasNextPage: false, endCursor: null };
              }
              return extractPageInfo(pageInfo);
            }
          );
    
          console.error(`[getUserComments] Found ${issueComments.length} issue comments for ${repo} after processing ${totalPRsProcessed} PRs`);
          allComments.push(...issueComments);
        }
      }
    
      console.error(`[getUserComments] Total comments before deduplication: ${allComments.length}`);
    
      // Step 3: Deduplicate comments (same comment ID might appear in both)
      const commentMap = new Map<string, UserComment>();
      for (const comment of allComments) {
        if (!commentMap.has(comment.id)) {
          commentMap.set(comment.id, comment);
        }
      }
    
      // Sort by createdAt
      const uniqueComments = Array.from(commentMap.values()).sort((a, b) => {
        return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
      });
    
      console.error(`[getUserComments] Completed: ${uniqueComments.length} unique comments found`);
      return { comments: uniqueComments };
    }
  • MCP server request handler registration for 'github.getUserComments' tool. Dispatches to GitHubTools.getUserComments and formats response as MCP content.
    case 'github.getUserComments': {
      const result = await tools.getUserComments(
        args.username as string,
        args.repos as string[],
        args.from as string | undefined,
        args.to as string | undefined
      );
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    }
  • Tool schema definition including input parameters (username, repo, from, to), description, and examples for the github.getUserComments tool.
          name: 'github.getUserComments',
          description: `Fetch all comments (review comments and issue comments) added by a user for a given repository within a time duration. Combines PR review comments (inline comments from PR reviews) and PR issue comments (general comments on PRs), normalizes them into a unified format, and deduplicates by comment ID. Filters by comment.createdAt and author. Automatically filters out auto-generated comments and comments on auto-created PRs. Use this tool to get a complete view of all user comments in a repository.
    
    Example use cases:
    - Get all comments by a user in a repository for analysis
    - Track comment activity and engagement
    - Analyze comment patterns and types
    - Extract all user feedback for sentiment analysis
    
    Returns: Array of comment objects with id, body, createdAt, author, prId, prNumber, prTitle, prRepo, commentType (review/issue), filePath, lineNumber, reviewId`,
          inputSchema: {
            type: 'object',
            properties: {
              username: {
                type: 'string',
                description: 'GitHub username (case-insensitive, @ prefix optional). Examples: "octocat", "@octocat"',
                examples: ['octocat', '@octocat'],
              },
              repo: {
                type: 'string',
                description: 'Repository in owner/repo format. Required - only comments for PRs in this repository will be returned. Example: "owner/repo"',
                examples: ['owner/repo', 'radireddy/AiApps'],
              },
              from: {
                type: 'string',
                description: 'Start timestamp in ISO 8601 format. Example: "2024-01-01T00:00:00Z"',
                examples: ['2024-01-01T00:00:00Z'],
              },
              to: {
                type: 'string',
                description: 'End timestamp in ISO 8601 format. Example: "2024-12-31T23:59:59Z"',
                examples: ['2024-12-31T23:59:59Z'],
              },
            },
            required: ['username', 'repo', 'from', 'to'],
          },
        },

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/radireddy/github-mcp'

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