Skip to content

Tool to search for issues #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 159 additions & 1 deletion src/api/tools/commonTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../utils/github.js";
import { fetchFileWithRobotsTxtCheck } from "../utils/robotsTxt.js";
import htmlToMd from "html-to-md";
import { searchCode } from "../utils/githubClient.js";
import { searchCode, searchIssues } from "../utils/githubClient.js";
import { fetchFileFromR2 } from "../utils/r2.js";
import { generateServerName } from "../../shared/nameUtils.js";
import {
Expand Down Expand Up @@ -708,6 +708,135 @@ export async function searchRepositoryCode({
}
}

/**
* Search for issues in a GitHub repository
* Supports filtering by issue state and pagination
Comment on lines +712 to +713
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The searchRepositoryIssues JSDoc is missing @param tags for each argument. Add parameter descriptions to clarify expected inputs and defaults.

Suggested change
* Search for issues in a GitHub repository
* Supports filtering by issue state and pagination
* Search for issues in a GitHub repository.
* Supports filtering by issue state and pagination.
*
* @param {RepoData} repoData - The repository data object containing owner and repo information.
* @param {string} query - The search query string to filter issues.
* @param {"open" | "closed" | "all"} [state="all"] - The state of issues to filter by. Defaults to "all".
* @param {number} [page=1] - The page number for pagination. Defaults to 1.
* @param {Env} env - The environment object containing configuration and secrets.
* @param {any} ctx - The context object for the request, used for logging and tracing.

Copilot uses AI. Check for mistakes.

*/
export async function searchRepositoryIssues({
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This function mixes data fetching and text formatting in one large block. Consider extracting the markdown‐generation logic into a helper to improve readability and testability.

Copilot uses AI. Check for mistakes.

repoData,
query,
state = "all",
page = 1,
env,
ctx,
}: {
repoData: RepoData;
query: string;
state?: "open" | "closed" | "all";
page?: number;
env: Env;
ctx: any;
}): Promise<{
searchQuery: string;
content: { type: "text"; text: string }[];
pagination?: {
totalCount: number;
currentPage: number;
perPage: number;
hasMorePages: boolean;
};
}> {
try {
const owner = repoData.owner;
const repo = repoData.repo;

if (!owner || !repo) {
return {
searchQuery: query,
content: [
{
type: "text" as const,
text: `### Issue Search Results for: "${query}"\n\nCannot perform issue search without repository information.`,
},
],
};
}

const currentPage = Math.max(1, page);
const resultsPerPage = 30;

const data = await searchIssues(
query,
owner,
repo,
env,
currentPage,
resultsPerPage,
state,
);

if (!data) {
return {
searchQuery: query,
content: [
{
type: "text" as const,
text: `### Issue Search Results for: "${query}"\n\nFailed to search issues in ${owner}/${repo}. GitHub API request failed.`,
},
],
};
}

if (data.total_count === 0 || !data.items || data.items.length === 0) {
return {
searchQuery: query,
content: [
{
type: "text" as const,
text: `### Issue Search Results for: "${query}"\n\nNo issues found in ${owner}/${repo}.`,
},
],
};
}

const totalCount = data.total_count;
const hasMorePages = currentPage * resultsPerPage < totalCount;
const totalPages = Math.ceil(totalCount / resultsPerPage);

let formattedResults = `### Issue Search Results for: "${query}"\n\n`;
formattedResults += `Found ${totalCount} issues in ${owner}/${repo}.\n`;
formattedResults += `Page ${currentPage} of ${totalPages}.\n\n`;

for (const item of data.items) {
formattedResults += `#### #${item.number}: ${item.title}\n`;
formattedResults += `- **State**: ${item.state}\n`;
formattedResults += `- **URL**: ${item.html_url}\n`;
formattedResults += `- **Score**: ${item.score}\n\n`;
}

if (hasMorePages) {
formattedResults += `_Showing ${data.items.length} of ${totalCount} results. Use pagination to see more results._\n\n`;
}

return {
searchQuery: query,
content: [
{
type: "text" as const,
text: formattedResults,
},
],
pagination: {
totalCount,
currentPage,
perPage: resultsPerPage,
hasMorePages,
},
};
} catch (error) {
console.error(`Error in searchRepositoryIssues: ${error}`);
return {
searchQuery: query,
content: [
{
type: "text" as const,
text: `### Issue Search Results for: "${query}"\n\nAn error occurred while searching issues: ${error}`,
},
],
};
}
}

export async function fetchUrlContent({ url, env }: { url: string; env: Env }) {
try {
// Use the robotsTxt checking function to respect robots.txt rules
Expand Down Expand Up @@ -992,6 +1121,35 @@ export function generateCodeSearchToolDescription({
return `Search for code within the GitHub repository: "${owner}/${repo}" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant.`;
}

/**
* Generate a dynamic tool name for the issue search tool based on the URL
*/
export function generateIssueSearchToolName({
urlType,
repo,
}: RepoData): string {
try {
let toolName = "search_issues";
if (urlType == "subdomain" || urlType == "github") {
return enforceToolNameLengthLimit("search_", repo, "_issues");
}
return toolName.replace(/[^a-zA-Z0-9]/g, "_");
} catch (error) {
console.error("Error generating issue search tool name:", error);
return "search_issues";
}
}

/**
* Generate a dynamic description for the issue search tool based on the URL
*/
export function generateIssueSearchToolDescription({
owner,
repo,
}: RepoData): string {
return `Search open or closed issues within the GitHub repository: "${owner}/${repo}".`;
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The default description only mentions open/closed states but not pagination or the all option. Align it with the generic tool description to avoid confusion.

Suggested change
return `Search open or closed issues within the GitHub repository: "${owner}/${repo}".`;
return `Search open or closed issues within the GitHub repository: "${owner}/${repo}". Supports pagination and the 'all' option to retrieve all matching issues.`;

Copilot uses AI. Check for mistakes.

}

/**
* Recursively list every subfolder prefix under `startPrefix`.
* @param {R2Bucket} bucket – the Workers-bound R2 bucket
Expand Down
12 changes: 12 additions & 0 deletions src/api/tools/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ describe("Tools Module", () => {
description:
'Search for code within the GitHub repository: "myorg/myrepo" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant.',
},
search_myrepo_issues: {
description:
'Search open or closed issues within the GitHub repository: "myorg/myrepo".',
},
},
},
// default handler - subdomain
Expand All @@ -83,6 +87,10 @@ describe("Tools Module", () => {
description:
'Search for code within the GitHub repository: "myorg/myrepo" using the GitHub Search API (exact match). Returns matching files for you to query further if relevant.',
},
search_myrepo_issues: {
description:
'Search open or closed issues within the GitHub repository: "myorg/myrepo".',
},
},
},
// generic handler
Expand All @@ -98,6 +106,10 @@ describe("Tools Module", () => {
description:
"Search for code in any GitHub repository by providing owner, project name, and search query. Returns matching files. Supports pagination with 30 results per page.",
},
search_generic_issues: {
description:
"Search issues in any GitHub repository by providing owner, project name, and search query. Supports filtering by state and pagination with 30 results per page.",
},
fetch_generic_url_content: {
description:
"Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation.",
Expand Down
37 changes: 37 additions & 0 deletions src/api/tools/repoHandlers/DefaultRepoHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import {
fetchDocumentation,
searchRepositoryDocumentation,
searchRepositoryCode,
searchRepositoryIssues,
fetchUrlContent,
generateFetchToolName,
generateFetchToolDescription,
generateSearchToolName,
generateSearchToolDescription,
generateCodeSearchToolName,
generateCodeSearchToolDescription,
generateIssueSearchToolName,
generateIssueSearchToolDescription,
} from "../commonTools.js";
import { z } from "zod";
import type { RepoData } from "../../../shared/repoData.js";
Expand All @@ -25,6 +28,9 @@ class DefaultRepoHandler implements RepoHandler {
const codeSearchToolName = generateCodeSearchToolName(repoData);
const codeSearchToolDescription =
generateCodeSearchToolDescription(repoData);
const issueSearchToolName = generateIssueSearchToolName(repoData);
const issueSearchToolDescription =
generateIssueSearchToolDescription(repoData);

return [
{
Expand Down Expand Up @@ -76,6 +82,37 @@ class DefaultRepoHandler implements RepoHandler {
});
},
},
{
name: issueSearchToolName,
description: issueSearchToolDescription,
paramsSchema: {
query: z
.string()
.describe("The search query to find relevant issues"),
state: z
.enum(["open", "closed", "all"])
.optional()
.describe(
"Filter issues by state. Defaults to all if not specified.",
),
page: z
.number()
.optional()
.describe(
"Page number to retrieve (starting from 1). Each page contains 30 results.",
),
},
cb: async ({ query, state, page }) => {
return searchRepositoryIssues({
repoData,
query,
state,
page,
env,
ctx,
});
},
},
];
}

Expand Down
43 changes: 43 additions & 0 deletions src/api/tools/repoHandlers/GenericRepoHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
fetchDocumentation,
searchRepositoryDocumentation,
searchRepositoryCode,
searchRepositoryIssues,
} from "../commonTools.js";
import { incrementRepoViewCount } from "../../utils/badge.js";
import rawMapping from "./generic/static-mapping.json";
Expand Down Expand Up @@ -151,6 +152,48 @@ class GenericRepoHandler implements RepoHandler {
return searchRepositoryCode({ repoData, query, page, env, ctx });
},
},
{
name: "search_generic_issues",
description:
"Search issues in any GitHub repository by providing owner, project name, and search query. Supports filtering by state and pagination with 30 results per page.",
paramsSchema: {
owner: z
.string()
.describe("The GitHub repository owner (username or organization)"),
repo: z.string().describe("The GitHub repository name"),
query: z
.string()
.describe("The search query to find relevant issues"),
state: z
.enum(["open", "closed", "all"])
.optional()
.describe(
"Filter issues by state. Defaults to all if not specified.",
),
page: z
.number()
.optional()
.describe(
"Page number to retrieve (starting from 1). Each page contains 30 results.",
),
},
cb: async ({ owner, repo, query, state, page }) => {
const repoData: RepoData = {
owner,
repo,
urlType: "github",
host: "gitmcp.io",
};
return searchRepositoryIssues({
repoData,
query,
state,
page,
env,
ctx,
});
},
},
];
}

Expand Down
42 changes: 42 additions & 0 deletions src/api/utils/githubClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,48 @@ export async function searchCode(
return response.json();
}

/**
* Search for issues in a GitHub repository
* @param query - Search query
* @param owner - Repository owner
* @param repo - Repository name
* @param env - Environment for GitHub token
* @param page - Page number (1-indexed)
* @param perPage - Results per page (max 100)
* @param state - Issue state filter
*/
export async function searchIssues(
query: string,
owner: string,
repo: string,
env: Env,
page: number = 1,
perPage: number = 30,
state: "open" | "closed" | "all" = "all",
): Promise<any> {
const validPerPage = Math.min(Math.max(1, perPage), 100);

let searchQuery = `${query} repo:${owner}/${repo} type:issue`;
if (state !== "all") {
searchQuery += ` state:${state}`;
}

const searchUrl =
`https://api.github.com/search/issues?q=${encodeURIComponent(searchQuery)}` +
`&page=${page}&per_page=${validPerPage}`;

const response = await githubApiRequest(searchUrl, {}, env);

if (!response || !response.ok) {
console.warn(
`GitHub API issue search failed: ${response?.status} ${response?.statusText}`,
);
return null;
}

return response.json();
}

/**
* Search for a specific filename in a GitHub repository
* @param filename - Filename to search for
Expand Down