Skip to main content
Glama
mapper.ts11 kB
/** * Data mappers to normalize GitHub GraphQL responses to MCP DTOs * * Maps GitHub GraphQL API objects to normalized DTOs for consistent MCP tool responses. * Reference: https://docs.github.com/en/graphql/reference/objects */ import { validateTimestamp } from '../utils/time.js'; // MCP DTOs - Normalized data structures for MCP tools export interface AuthoredPR { id: string; repo: string; title: string; createdAt: string; mergedAt: string | null; state: 'OPEN' | 'MERGED' | 'CLOSED'; filesChanged: number; additions: number; deletions: number; } export interface PRReview { id: string; state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'; prId: string; prNumber: number; prTitle: string; prRepo: string; submittedAt: string; } export interface ReviewComment { id: string; body: string; filePath: string | null; lineNumber: number | null; prId: string; prNumber: number; prTitle: string; prRepo: string; prUrl: string; prCreatedAt: string; timestamp: string; reviewId: string; } export interface CommentImpact { commentId: string; prId: string; hadImpact: boolean; confidence: number; evidence: string[]; } export interface CommentImpactResponse { impacts: CommentImpact[]; stats?: { totalComments: number; totalPRsReviewed: number; totalImpacts: number; }; } export interface UserComment { id: string; body: string; createdAt: string; author: string; prId: string; prNumber: number; prTitle: string; prRepo: string; commentType: 'review' | 'issue'; filePath: string | null; lineNumber: number | null; reviewId: string | null; } export interface UserRepoStats { username: string; repo: string; timeRange: { from: string; to: string; }; prs: { count: number; merged: number; open: number; closed: number; }; comments: { total: number; review: number; issue: number; }; reviews: { total: number; totalPRsReviewed: number; approved: number; changesRequested: number; commented: number; }; codeChanges: { filesChanged: number; additions: number; deletions: number; netChange: number; }; } export interface PRReviewComments { prId: string; prNumber: number; prTitle: string; prRepo: string; prUrl: string; prCreatedAt: string; comments: string[]; totalComments: number; } export interface ReviewCommentsResponse { userId: string; dateRange: { from: string; to: string; }; totalPRsReviewed: number; totalComments: number; prs: PRReviewComments[]; } /** * Maps GitHub PullRequest node to AuthoredPR DTO * * Reference: https://docs.github.com/en/graphql/reference/objects#pullrequest */ export function mapAuthoredPR(node: any): AuthoredPR { // Map GitHub PR state to normalized state const state = node.state === 'MERGED' ? 'MERGED' : node.state === 'CLOSED' ? 'CLOSED' : 'OPEN'; return { id: node.id, repo: node.repository?.nameWithOwner || 'unknown', title: node.title || '', createdAt: validateTimestamp(node.createdAt), mergedAt: node.mergedAt ? validateTimestamp(node.mergedAt) : null, state, filesChanged: node.changedFiles || node.files?.totalCount || 0, additions: node.additions || 0, deletions: node.deletions || 0, }; } /** * Maps GitHub PullRequestReview contribution to PRReview DTO * * Reference: * - https://docs.github.com/en/graphql/reference/objects#pullrequestreview * - https://docs.github.com/en/graphql/reference/objects#pullrequestreviewcontribution */ export function mapPRReview(contribution: any): PRReview { const review = contribution.pullRequestReview; const pr = review.pullRequest; // Map GitHub review state to our enum // States: APPROVED, CHANGES_REQUESTED, COMMENTED, DISMISSED, PENDING let state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'; switch (review.state) { case 'APPROVED': state = 'APPROVED'; break; case 'CHANGES_REQUESTED': state = 'CHANGES_REQUESTED'; break; default: state = 'COMMENTED'; } return { id: review.id, state, prId: pr.id, prNumber: pr.number, prTitle: pr.title || '', prRepo: pr.repository?.nameWithOwner || 'unknown', submittedAt: validateTimestamp(review.submittedAt), }; } /** * Maps GitHub review comments to ReviewComment DTOs * * Extracts both general review body comments and inline review comments. * Reference: https://docs.github.com/en/graphql/reference/objects#pullrequestreviewcomment */ export function mapReviewComments(contribution: any): ReviewComment[] { const review = contribution.pullRequestReview; const pr = review.pullRequest; const comments: ReviewComment[] = []; const prCreatedAt = pr.createdAt ? validateTimestamp(pr.createdAt) : ''; const prUrl = pr.url || ''; // Add general review comment if body exists if (review.body && review.body.trim()) { comments.push({ id: `${review.id}-body`, body: review.body, filePath: null, lineNumber: null, prId: pr.id, prNumber: pr.number, prTitle: pr.title || '', prRepo: pr.repository?.nameWithOwner || 'unknown', prUrl, prCreatedAt, timestamp: validateTimestamp(review.submittedAt), reviewId: review.id, }); } // Add inline comments if (review.comments?.nodes) { for (const comment of review.comments.nodes) { comments.push({ id: comment.id, body: comment.body || '', filePath: comment.path || null, lineNumber: comment.line || null, prId: pr.id, prNumber: pr.number, prTitle: pr.title || '', prRepo: pr.repository?.nameWithOwner || 'unknown', prUrl, prCreatedAt, timestamp: validateTimestamp(comment.createdAt), reviewId: review.id, }); } } return comments; } /** * Analyzes comment impact based on PR timeline * * Heuristic: Checks if commits were made after a comment timestamp. * Higher confidence if comment is on a specific file/line and that file was modified. * * Reference: https://docs.github.com/en/graphql/reference/objects#pullrequesttimelineitems */ export function analyzeCommentImpact( comment: ReviewComment, prTimeline: any ): CommentImpact { const evidence: string[] = []; let hadImpact = false; let confidence = 0; if (!prTimeline?.timelineItems?.nodes) { // Return hadImpact: false with empty evidence - caller will filter these out return { commentId: comment.id, prId: comment.prId, hadImpact: false, confidence: 0, evidence: [], }; } const commentTime = new Date(comment.timestamp).getTime(); // Get commits from PR commits (more reliable than timeline items) // Commits are ordered chronologically, so we can check if any came after the comment const commits = prTimeline.commits?.nodes || []; // Also check timeline items for commits (fallback) const timelineCommits = (prTimeline.timelineItems?.nodes || []).filter( (item: any) => item.__typename === 'PullRequestCommit' ); // Combine both sources and deduplicate by commit ID const allCommits = new Map<string, any>(); for (const commitNode of commits) { if (commitNode?.commit?.id) { allCommits.set(commitNode.commit.id, commitNode.commit); } } for (const commitItem of timelineCommits) { if (commitItem?.commit?.id) { allCommits.set(commitItem.commit.id, commitItem.commit); } } for (const commit of allCommits.values()) { // Use committedDate if available (when commit was actually committed/pushed to branch) // Otherwise fall back to authoredDate (when commit was originally authored) // Note: authoredDate can be earlier than when commit was pushed to PR, so we prefer committedDate const commitDateStr = commit.committedDate || commit.authoredDate; if (!commitDateStr) continue; const commitTime = new Date(commitDateStr).getTime(); // Skip commits that were made before or at the same time as the comment // We want commits that happened AFTER the comment to show impact // Using <= to skip commits at the same timestamp (allowing for small timing differences) if (commitTime <= commentTime) { continue; } // Found a commit after the comment - this indicates potential impact // If comment is on a specific file/line, check if that file was modified if (comment.filePath) { // Check if file was in commit changes // Note: GitHub GraphQL doesn't expose file-level commit details easily // This is a simplified heuristic - higher confidence if files were changed if (commit.changedFiles > 0) { evidence.push( `Commit ${commit.id.substring(0, 7)} modified files after comment (${commit.changedFiles} files)` ); hadImpact = true; confidence = 0.5; // Medium confidence without file-level detail } } else { // General comment - lower confidence if (commit.changedFiles > 0) { evidence.push( `General comment followed by commit ${commit.id.substring(0, 7)}` ); hadImpact = true; confidence = 0.3; } } } // Higher confidence if comment requested changes (would need additional query to verify) if (hadImpact && comment.reviewId) { confidence = Math.min(confidence + 0.2, 1.0); } // Only return impact if there was actual impact (commits found after comment) // Don't include "No commits found" message - just return hadImpact: false // The caller will filter these out return { commentId: comment.id, prId: comment.prId, hadImpact, confidence, evidence, }; } /** * Maps review comment from PR review to UserComment DTO */ export function mapReviewCommentToUserComment( comment: any, pr: any, reviewId: string | null ): UserComment { return { id: comment.id, body: comment.body || '', createdAt: validateTimestamp(comment.createdAt), author: comment.author?.login || 'unknown', prId: pr.id, prNumber: pr.number, prTitle: pr.title || '', prRepo: pr.repository?.nameWithOwner || 'unknown', commentType: 'review', filePath: comment.path || null, lineNumber: comment.line || null, reviewId: reviewId, }; } /** * Maps issue comment from PR to UserComment DTO */ export function mapIssueCommentToUserComment( comment: any, pr: any ): UserComment { return { id: comment.id, body: comment.body || '', createdAt: validateTimestamp(comment.createdAt), author: comment.author?.login || 'unknown', prId: pr.id, prNumber: pr.number, prTitle: pr.title || '', prRepo: pr.repository?.nameWithOwner || 'unknown', commentType: 'issue', filePath: null, lineNumber: null, reviewId: null, }; }

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