find-similar-issues
Identify similar GitHub issues by comparing new issue descriptions to existing ones, helping prioritize and resolve problems efficiently within a repository.
Instructions
Find GitHub issues similar to a new issue description
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| issueDescription | Yes | Description of the issue to find similar ones for | |
| maxResults | No | Maximum number of similar issues to return | |
| owner | Yes | GitHub repository owner/organization | |
| repo | Yes | GitHub repository name |
Implementation Reference
- src/index.ts:114-187 (handler)The main handler function that implements the 'find-similar-issues' tool logic, searching GitHub for similar issues using keyword extraction, API search, similarity scoring, and formatting results.async ({ owner, repo, issueDescription, maxResults }) => { // Combine title and description for better search results const searchText = `${issueDescription}`; // Extract important keywords for search (simple approach) const keywords = searchText .toLowerCase() .replace(/[^\w\s]/g, '') .split(/\s+/) .filter(word => word.length > 3) // Filter out short words .filter(word => !['the', 'and', 'that', 'this', 'with'].includes(word)) // Filter common words .slice(0, 10) // Limit number of keywords .join(' '); // Search for issues in the repository const searchParams = { q: `repo:${owner}/${repo} ${keywords}`, sort: 'updated', order: 'desc', per_page: '30' // Get more results to filter by similarity }; const searchResponse = await makeGithubRequest<GithubSearchResponse>( '/search/issues', searchParams ); if (!searchResponse) { return { content: [ { type: "text", text: "Failed to retrieve issues from GitHub" } ] }; } if (searchResponse.total_count === 0) { return { content: [ { type: "text", text: `No similar issues found in ${owner}/${repo}` } ] }; } // Calculate similarity score for each issue const issuesWithScores = searchResponse.items .map(issue => ({ issue, score: calculateSimilarity(searchText, `${issue.title} ${issue.body || ''}`) })) .sort((a, b) => b.score - a.score) // Sort by similarity score (highest first) .slice(0, maxResults); // Take top N results // Format the response const formattedIssues = issuesWithScores.map(({ issue, score }) => formatIssue(issue, score) ); const responseText = `Found ${issuesWithScores.length} similar issues in ${owner}/${repo}:\n\n${formattedIssues.join("\n")}`; return { content: [ { type: "text", text: responseText } ] }; }
- src/index.ts:108-113 (schema)Zod input schema defining parameters: owner, repo, issueDescription, and maxResults for the tool.{ owner: z.string().describe("GitHub repository owner/organization"), repo: z.string().describe("GitHub repository name"), issueDescription: z.string().describe("Description of the issue to find similar ones for"), maxResults: z.number().int().min(1).max(20).default(5).describe("Maximum number of similar issues to return") },
- src/index.ts:105-188 (registration)Registration of the 'find-similar-issues' tool using McpServer.tool() with name, description, input schema, and inline handler.server.tool( "find-similar-issues", "Find GitHub issues similar to a new issue description", { owner: z.string().describe("GitHub repository owner/organization"), repo: z.string().describe("GitHub repository name"), issueDescription: z.string().describe("Description of the issue to find similar ones for"), maxResults: z.number().int().min(1).max(20).default(5).describe("Maximum number of similar issues to return") }, async ({ owner, repo, issueDescription, maxResults }) => { // Combine title and description for better search results const searchText = `${issueDescription}`; // Extract important keywords for search (simple approach) const keywords = searchText .toLowerCase() .replace(/[^\w\s]/g, '') .split(/\s+/) .filter(word => word.length > 3) // Filter out short words .filter(word => !['the', 'and', 'that', 'this', 'with'].includes(word)) // Filter common words .slice(0, 10) // Limit number of keywords .join(' '); // Search for issues in the repository const searchParams = { q: `repo:${owner}/${repo} ${keywords}`, sort: 'updated', order: 'desc', per_page: '30' // Get more results to filter by similarity }; const searchResponse = await makeGithubRequest<GithubSearchResponse>( '/search/issues', searchParams ); if (!searchResponse) { return { content: [ { type: "text", text: "Failed to retrieve issues from GitHub" } ] }; } if (searchResponse.total_count === 0) { return { content: [ { type: "text", text: `No similar issues found in ${owner}/${repo}` } ] }; } // Calculate similarity score for each issue const issuesWithScores = searchResponse.items .map(issue => ({ issue, score: calculateSimilarity(searchText, `${issue.title} ${issue.body || ''}`) })) .sort((a, b) => b.score - a.score) // Sort by similarity score (highest first) .slice(0, maxResults); // Take top N results // Format the response const formattedIssues = issuesWithScores.map(({ issue, score }) => formatIssue(issue, score) ); const responseText = `Found ${issuesWithScores.length} similar issues in ${owner}/${repo}:\n\n${formattedIssues.join("\n")}`; return { content: [ { type: "text", text: responseText } ] }; } );
- src/index.ts:72-86 (helper)Helper function to calculate Jaccard similarity score between issue description and candidate issues for ranking.function calculateSimilarity(text1: string, text2: string): number { // Convert to lowercase and remove special characters for comparison const normalize = (text: string) => text.toLowerCase().replace(/[^\w\s]/g, ''); const words1 = new Set(normalize(text1).split(/\s+/)); const words2 = new Set(normalize(text2).split(/\s+/)); // Find intersection of words const intersection = new Set([...words1].filter(word => words2.has(word))); // Calculate Jaccard similarity coefficient const union = new Set([...words1, ...words2]); return intersection.size / union.size; }
- src/index.ts:17-51 (helper)Helper function for making authenticated GitHub API requests, used by the tool for searching issues.async function makeGithubRequest<T>(path: string, params: Record<string, string> = {}): Promise<T | null> { const url = new URL(path, GITHUB_API_BASE); // Add query parameters Object.entries(params).forEach(([key, value]) => { if (value) url.searchParams.append(key, value); }); const headers: { "User-Agent": string; "Accept": string; "X-GitHub-Api-Version": string; Authorization?: string; } = { "User-Agent": USER_AGENT, "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" }; // Add authorization if token is available if (GITHUB_TOKEN) { headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`; } try { const response = await fetch(url.toString(), { headers }); if (!response.ok) { throw new Error(`GitHub API error! status: ${response.status}`); } return (await response.json()) as T; } catch (error) { console.error("Error making GitHub request:", error); return null; } }