search_reports
Search HackerOne vulnerability reports using filters for keywords, programs, severity, or state to find past reports for reference when drafting new ones.
Instructions
Search and list your HackerOne reports. Filter by keyword, program, severity, or state. Great for finding past reports to reference when drafting new ones.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Keyword search (e.g. 'SSRF', 'OAuth', 'PassRole', 'S3') | |
| program | No | Program handle to filter by (e.g. 'uber', 'amazon') | |
| severity | No | Filter by severity rating | |
| state | No | Filter by report state | |
| page_size | No | Results per page (default 25) | |
| page_number | No | Page number for pagination | |
| sort | No | Sort field (e.g. 'reports.created_at' or '-reports.created_at' for desc) |
Implementation Reference
- src/h1client.ts:50-178 (handler)The core implementation of `searchReports`, which handles pagination and client-side filtering/sorting as the HackerOne API does not support server-side filtering for reports.
export async function searchReports(opts: SearchReportsOpts = {}) { // The /hackers/me/reports endpoint only supports pagination (page[number], page[size]). // Filtering by program, severity, state, keyword must be done client-side. const needsFilter = !!(opts.program || opts.severity || opts.state || opts.query); const requestedSize = opts.page_size ?? 25; // If filtering, fetch max results to filter from; otherwise respect page_size const fetchSize = needsFilter ? 100 : requestedSize; const pageNumber = opts.page_number ?? 1; let allReports: any[] = []; if (needsFilter) { // H1 hacker API doesn't support server-side filtering or sorting. // Strategy: find the last page first, then fetch backwards (newest first) // so recent reports are found quickly without fetching all 900+ reports. // Step 1: find total pages by probing let lastPage = 1; const probeRes = await h1Fetch("/hackers/me/reports", { "page[size]": "100", "page[number]": "1", }); if (probeRes.data?.length === 100) { // Binary search for last page let lo = 1, hi = 50; while (lo < hi) { const mid = Math.ceil((lo + hi) / 2); const check = await h1Fetch("/hackers/me/reports", { "page[size]": "100", "page[number]": String(mid), }); if (check.data?.length > 0) { lo = mid; if (check.data.length < 100) break; // This is the last page hi = Math.max(hi, mid + 5); } else { hi = mid - 1; } } lastPage = lo; } // Step 2: fetch from last page backwards (newest reports first) for (let page = lastPage; page >= 1; page--) { const data = page === 1 && probeRes.data ? probeRes // reuse first page probe if we loop back to it : await h1Fetch("/hackers/me/reports", { "page[size]": "100", "page[number]": String(page), }); if (!data.data || data.data.length === 0) continue; allReports.push(...data.data); // Early exit: check if we already have enough matches const tempFiltered = allReports.filter((r: any) => { const prog = r.relationships?.program?.data?.attributes?.handle; if (opts.program && prog?.toLowerCase() !== opts.program.toLowerCase()) return false; if (opts.severity && r.attributes.severity_rating !== opts.severity) return false; if (opts.state && r.attributes.state !== opts.state) return false; return true; }); if (tempFiltered.length >= requestedSize) break; } } else { const data = await h1Fetch("/hackers/me/reports", { "page[size]": String(fetchSize), "page[number]": String(pageNumber), }); allReports = data.data ?? []; } // Map to clean objects — keep vulnerability_information for keyword filtering but strip from final output let reports = allReports.map((r: any) => ({ id: r.id, title: r.attributes.title, state: r.attributes.state, substate: r.attributes.substate, severity: r.attributes.severity_rating, created_at: r.attributes.created_at, disclosed_at: r.attributes.disclosed_at, bounty_awarded_at: r.attributes.bounty_awarded_at, _vuln_info: r.attributes.vulnerability_information, weakness: r.relationships?.weakness?.data?.attributes?.name ?? null, program: r.relationships?.program?.data?.attributes?.handle ?? null, })); // Client-side filtering if (opts.program) { const prog = opts.program.toLowerCase(); reports = reports.filter((r) => r.program?.toLowerCase() === prog); } if (opts.severity) { reports = reports.filter((r) => r.severity === opts.severity); } if (opts.state) { reports = reports.filter((r) => r.state === opts.state); } if (opts.query) { const q = opts.query.toLowerCase(); reports = reports.filter( (r) => r.title?.toLowerCase().includes(q) || r._vuln_info?.toLowerCase().includes(q) || r.weakness?.toLowerCase().includes(q) ); } // Sort if requested if (opts.sort) { const desc = opts.sort.startsWith("-"); const field = opts.sort.replace(/^-/, "").replace("reports.", ""); reports.sort((a: any, b: any) => { const va = a[field] ?? ""; const vb = b[field] ?? ""; return desc ? (vb > va ? 1 : -1) : (va > vb ? 1 : -1); }); } // Apply page_size limit to filtered results if (needsFilter) { reports = reports.slice(0, requestedSize); } // Strip internal _vuln_info from output to keep responses small return reports.map(({ _vuln_info, ...rest }) => rest); } - src/index.ts:23-86 (registration)Tool registration for `search_reports` using the MCP server SDK in `src/index.ts`, defining schemas using Zod and invoking the `searchReports` handler.
server.tool( "search_reports", "Search and list your HackerOne reports. Filter by keyword, program, severity, or state. Great for finding past reports to reference when drafting new ones.", { query: z .string() .optional() .describe( "Keyword search (e.g. 'SSRF', 'OAuth', 'PassRole', 'S3')" ), program: z .string() .optional() .describe("Program handle to filter by (e.g. 'uber', 'amazon')"), severity: z .enum(["none", "low", "medium", "high", "critical"]) .optional() .describe("Filter by severity rating"), state: z .enum([ "new", "triaged", "needs-more-info", "resolved", "not-applicable", "informative", "duplicate", "spam", ]) .optional() .describe("Filter by report state"), page_size: z .number() .min(1) .max(100) .optional() .describe("Results per page (default 25)"), page_number: z.number().optional().describe("Page number for pagination"), sort: z .string() .optional() .describe( "Sort field (e.g. 'reports.created_at' or '-reports.created_at' for desc)" ), }, async (params) => { try { const results = await searchReports(params); return { content: [ { type: "text" as const, text: JSON.stringify(results, null, 2), }, ], }; } catch (err: any) { return { content: [{ type: "text" as const, text: `Error: ${err.message}` }], isError: true, }; } } );