hackerone-mcp-server 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -38
- package/dist/h1client.d.ts +40 -0
- package/dist/h1client.js +259 -0
- package/dist/index.js +21 -2
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
# HackerOne MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MCP server that gives Claude Code (or any MCP client) full access to your HackerOne reports, programs, earnings, and scope data via the HackerOne API — including submitting reports and responding to triage.
|
|
3
|
+
Unofficial MCP server that gives Codex, Claude Code, or any MCP client full access to your HackerOne reports, programs, earnings, and scope data via the HackerOne API — including submitting reports and responding to triage.
|
|
6
4
|
|
|
7
5
|
## Setup
|
|
8
6
|
|
|
7
|
+
Install Node.js and npm, then use the published [hackerone-mcp-server package](https://www.npmjs.com/package/hackerone-mcp-server) through `npx`.
|
|
8
|
+
|
|
9
9
|
### 1. Get your HackerOne API token
|
|
10
10
|
|
|
11
11
|
Go to **HackerOne > Settings > API Token** and generate one.
|
|
12
12
|
|
|
13
|
-
### 2.
|
|
13
|
+
### 2. Add to your MCP client
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
git clone https://github.com/Sicks3c/hackerone-mcp-server.git
|
|
17
|
-
cd hackerone-mcp-server
|
|
18
|
-
npm install
|
|
19
|
-
npm run build
|
|
20
|
-
```
|
|
15
|
+
Replace `your-username` and `your-api-token` below with your HackerOne credentials.
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
#### Codex
|
|
23
18
|
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
```toml
|
|
20
|
+
[mcp_servers.hackerone]
|
|
21
|
+
command = "npx"
|
|
22
|
+
args = ["-y", "hackerone-mcp-server@latest"]
|
|
23
|
+
startup_timeout_sec = 60
|
|
24
|
+
env = { "H1_USERNAME" = "your-username", "H1_API_TOKEN" = "your-api-token" }
|
|
30
25
|
```
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
#### Claude Code
|
|
33
28
|
|
|
34
29
|
```json
|
|
35
30
|
{
|
|
36
31
|
"mcpServers": {
|
|
37
32
|
"hackerone": {
|
|
38
|
-
"command": "
|
|
39
|
-
"args": ["
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "hackerone-mcp-server@latest"],
|
|
40
35
|
"env": {
|
|
41
36
|
"H1_USERNAME": "your-username",
|
|
42
37
|
"H1_API_TOKEN": "your-api-token"
|
|
@@ -46,14 +41,21 @@ Or add manually to `~/.claude.json`:
|
|
|
46
41
|
}
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
## Build from source
|
|
45
|
+
|
|
46
|
+
For local development:
|
|
50
47
|
|
|
51
48
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
git clone https://github.com/11philip22/hackerone-mcp-server.git
|
|
50
|
+
cd hackerone-mcp-server
|
|
51
|
+
npm install
|
|
52
|
+
npm run build
|
|
55
53
|
```
|
|
56
54
|
|
|
55
|
+
Run `npm test` to build and run the fixture tests. Tests use Node's experimental module mocking and require Node.js 22.3+ (or 20.18+ on the Node 20 release line).
|
|
56
|
+
|
|
57
|
+
To use your local build, set the MCP command to `node` and its arguments to `["/absolute/path/to/hackerone-mcp-server/dist/index.js"]`, keeping the same environment variables.
|
|
58
|
+
|
|
57
59
|
## Tools
|
|
58
60
|
|
|
59
61
|
### Read
|
|
@@ -67,6 +69,7 @@ claude
|
|
|
67
69
|
| `list_programs` | List all bug bounty programs you have access to (auto-paginates) |
|
|
68
70
|
| `get_program_details` | Get single program info: policy, response times, metrics |
|
|
69
71
|
| `get_program_signal_requirements` | Get a public program's minimum past-year signal threshold and whether requirements are enabled |
|
|
72
|
+
| `get_program_rewards` | Get a public program's currency, severity payout ranges, reward groups, and each group's scope assets |
|
|
70
73
|
| `get_program_scope` | Get all in-scope assets for a program (auto-paginates) |
|
|
71
74
|
| `get_program_weaknesses` | Get accepted CWE/weakness types for a program (auto-paginates) |
|
|
72
75
|
| `get_earnings` | Get your bounty earnings history (amounts, dates, programs) |
|
|
@@ -125,6 +128,11 @@ Show my hacker profile — what's my current reputation and signal?
|
|
|
125
128
|
What signal do I need to submit reports to the uber program?
|
|
126
129
|
```
|
|
127
130
|
|
|
131
|
+
**Compare rewards by asset group:**
|
|
132
|
+
```
|
|
133
|
+
Show TikTok's reward groups, severity payout ranges, and which Android apps and websites belong to each group. Flag any unavailable data.
|
|
134
|
+
```
|
|
135
|
+
|
|
128
136
|
**Track earnings:**
|
|
129
137
|
```
|
|
130
138
|
Show my recent bounty earnings and current balance
|
|
@@ -134,17 +142,3 @@ Show my recent bounty earnings and current balance
|
|
|
134
142
|
```
|
|
135
143
|
Analyze my report patterns — what severity gets resolved most?
|
|
136
144
|
```
|
|
137
|
-
|
|
138
|
-
## How It Works
|
|
139
|
-
|
|
140
|
-
- Connects to the [HackerOne Hacker API v1](https://api.hackerone.com/hacker-resources/) using your personal API token
|
|
141
|
-
- Runs locally over stdio — your credentials never leave your machine
|
|
142
|
-
- Supports both read and write operations (submit reports, add comments, close reports)
|
|
143
|
-
- Auto-paginates programs, scope, and weakness endpoints so nothing gets silently truncated
|
|
144
|
-
- Uses server-side API filters where available (program, severity, state) for faster searches
|
|
145
|
-
- Built-in retry with exponential backoff for rate limit handling
|
|
146
|
-
- 60-second response cache to reduce redundant API calls
|
|
147
|
-
|
|
148
|
-
## License
|
|
149
|
-
|
|
150
|
-
MIT
|
package/dist/h1client.d.ts
CHANGED
|
@@ -81,6 +81,45 @@ export declare function getProgramDetails(handle: string): Promise<{
|
|
|
81
81
|
bookmarked: any;
|
|
82
82
|
}>;
|
|
83
83
|
export declare function getProgramSignalRequirements(handle: string): Promise<any>;
|
|
84
|
+
type RewardAvailability = "available" | "partial" | "unavailable";
|
|
85
|
+
type RewardSeverity = "low" | "medium" | "high" | "critical";
|
|
86
|
+
interface RewardAsset {
|
|
87
|
+
id: string;
|
|
88
|
+
asset_identifier: string;
|
|
89
|
+
asset_type: string | null;
|
|
90
|
+
eligible_for_submission: boolean | null;
|
|
91
|
+
eligible_for_bounty: boolean | null;
|
|
92
|
+
}
|
|
93
|
+
interface RewardGroup {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string | null;
|
|
96
|
+
description: string | null;
|
|
97
|
+
scope_kind: "all_assets" | "asset" | "reward_group" | null;
|
|
98
|
+
use_range: boolean | null;
|
|
99
|
+
rewards: Record<RewardSeverity, {
|
|
100
|
+
label: string | null;
|
|
101
|
+
minimum: number | null;
|
|
102
|
+
maximum: number | null;
|
|
103
|
+
}>;
|
|
104
|
+
assets: RewardAsset[] | null;
|
|
105
|
+
assets_availability: RewardAvailability;
|
|
106
|
+
}
|
|
107
|
+
export interface ProgramRewards {
|
|
108
|
+
handle: string;
|
|
109
|
+
currency: string | null;
|
|
110
|
+
source: string;
|
|
111
|
+
program_url: string;
|
|
112
|
+
format: "legacy" | "scope_and_rewards" | null;
|
|
113
|
+
description: string | null;
|
|
114
|
+
availability: RewardAvailability;
|
|
115
|
+
issues: {
|
|
116
|
+
code: string;
|
|
117
|
+
group_id: string | null;
|
|
118
|
+
message: string;
|
|
119
|
+
}[];
|
|
120
|
+
groups: RewardGroup[] | null;
|
|
121
|
+
}
|
|
122
|
+
export declare function getProgramRewards(handle: string): Promise<ProgramRewards>;
|
|
84
123
|
export declare function getProgramScope(handle: string, pageSize?: number): Promise<{
|
|
85
124
|
id: any;
|
|
86
125
|
asset_type: any;
|
|
@@ -177,3 +216,4 @@ export declare function searchDisclosedReports(opts: {
|
|
|
177
216
|
query?: string;
|
|
178
217
|
page_size?: number;
|
|
179
218
|
}): Promise<any>;
|
|
219
|
+
export {};
|
package/dist/h1client.js
CHANGED
|
@@ -482,6 +482,265 @@ export async function getProgramSignalRequirements(handle) {
|
|
|
482
482
|
cacheSet(cacheKey, result);
|
|
483
483
|
return result;
|
|
484
484
|
}
|
|
485
|
+
// ── Get public program rewards ───────────────────────────────────
|
|
486
|
+
export async function getProgramRewards(handle) {
|
|
487
|
+
handle = handle.trim();
|
|
488
|
+
if (!handle)
|
|
489
|
+
throw new Error("program_handle must not be empty");
|
|
490
|
+
const cacheKey = `program-rewards:${handle}`;
|
|
491
|
+
const cached = cacheGet(cacheKey);
|
|
492
|
+
if (cached)
|
|
493
|
+
return cached;
|
|
494
|
+
// shortcut: uses the public website API; switch when the Hacker API exposes reward groups.
|
|
495
|
+
const result = {
|
|
496
|
+
handle, currency: null, source: "https://hackerone.com/graphql",
|
|
497
|
+
program_url: `https://hackerone.com/${encodeURIComponent(handle)}`,
|
|
498
|
+
format: null, description: null, availability: "unavailable", issues: [], groups: null,
|
|
499
|
+
};
|
|
500
|
+
const issue = (code, message, group_id = null) => {
|
|
501
|
+
result.issues.push({ code, group_id, message });
|
|
502
|
+
};
|
|
503
|
+
const request = async (query, variables = {}) => {
|
|
504
|
+
const res = await fetch(result.source, {
|
|
505
|
+
method: "POST", redirect: "error", signal: AbortSignal.timeout(15_000),
|
|
506
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
507
|
+
body: JSON.stringify({ query, variables: { handle, ...variables } }),
|
|
508
|
+
});
|
|
509
|
+
if (!res.ok)
|
|
510
|
+
throw new Error(`HackerOne GraphQL HTTP error ${res.status}`);
|
|
511
|
+
const body = await res.json();
|
|
512
|
+
if (body?.errors !== undefined && (!Array.isArray(body.errors) || body.errors.length)) {
|
|
513
|
+
if (body.data?.team === null && Array.isArray(body.errors) &&
|
|
514
|
+
body.errors.length && body.errors.every((e) => e?.type === "NOT_FOUND"))
|
|
515
|
+
return null;
|
|
516
|
+
throw new Error("HackerOne GraphQL returned errors");
|
|
517
|
+
}
|
|
518
|
+
const team = body?.data?.team;
|
|
519
|
+
if (team !== null && !isObject(team))
|
|
520
|
+
throw new Error("Malformed HackerOne team response");
|
|
521
|
+
return team;
|
|
522
|
+
};
|
|
523
|
+
const scopeFields = `_id asset_identifier asset_type eligible_for_submission eligible_for_bounty`;
|
|
524
|
+
const rewardsQuery = `query ProgramRewards($handle: String!, $after: String) {
|
|
525
|
+
team(handle: $handle) {
|
|
526
|
+
handle currency scope_and_rewards_groups_enabled
|
|
527
|
+
bounty_table {
|
|
528
|
+
_id description low_label medium_label high_label critical_label
|
|
529
|
+
bounty_table_rows(first: 100, after: $after) {
|
|
530
|
+
pageInfo { hasNextPage endCursor }
|
|
531
|
+
edges { node {
|
|
532
|
+
_id name description use_range
|
|
533
|
+
low low_minimum medium medium_minimum high high_minimum critical critical_minimum
|
|
534
|
+
structured_scope { ${scopeFields} }
|
|
535
|
+
} }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}`;
|
|
540
|
+
const rows = new Map();
|
|
541
|
+
const groups = [];
|
|
542
|
+
const nextCursor = (connection, seen) => {
|
|
543
|
+
const info = connection?.pageInfo;
|
|
544
|
+
if (!isObject(info) || typeof info.hasNextPage !== "boolean") {
|
|
545
|
+
throw new Error("Missing reward pagination metadata");
|
|
546
|
+
}
|
|
547
|
+
if (!connection.edges.length && seen.size)
|
|
548
|
+
throw new Error("Prematurely empty reward page");
|
|
549
|
+
if (!info.hasNextPage)
|
|
550
|
+
return null;
|
|
551
|
+
if (!connection.edges.length || typeof info.endCursor !== "string" ||
|
|
552
|
+
!info.endCursor || seen.has(info.endCursor)) {
|
|
553
|
+
throw new Error("Incomplete or repeated reward pagination cursor");
|
|
554
|
+
}
|
|
555
|
+
seen.add(info.endCursor);
|
|
556
|
+
return info.endCursor;
|
|
557
|
+
};
|
|
558
|
+
const nodes = (connection) => {
|
|
559
|
+
if (!isObject(connection) || !Array.isArray(connection.edges) ||
|
|
560
|
+
connection.edges.some((e) => !isObject(e?.node))) {
|
|
561
|
+
throw new Error("Malformed reward connection");
|
|
562
|
+
}
|
|
563
|
+
return connection.edges.map((e) => e.node);
|
|
564
|
+
};
|
|
565
|
+
const asset = (raw) => {
|
|
566
|
+
if (!isObject(raw) || typeof raw._id !== "string" || !raw._id ||
|
|
567
|
+
typeof raw.asset_identifier !== "string" || !raw.asset_identifier) {
|
|
568
|
+
throw new Error("Malformed reward scope asset");
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
id: raw._id, asset_identifier: raw.asset_identifier,
|
|
572
|
+
asset_type: typeof raw.asset_type === "string" ? raw.asset_type : null,
|
|
573
|
+
eligible_for_submission: typeof raw.eligible_for_submission === "boolean" ? raw.eligible_for_submission : null,
|
|
574
|
+
eligible_for_bounty: typeof raw.eligible_for_bounty === "boolean" ? raw.eligible_for_bounty : null,
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
let team = await request(rewardsQuery);
|
|
579
|
+
if (team === null) {
|
|
580
|
+
issue("not_public_or_not_found", "Program was not found or is not publicly accessible");
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
if (typeof team.handle !== "string" || typeof team.scope_and_rewards_groups_enabled !== "boolean") {
|
|
584
|
+
throw new Error("Malformed program rewards metadata");
|
|
585
|
+
}
|
|
586
|
+
result.handle = team.handle;
|
|
587
|
+
result.format = team.scope_and_rewards_groups_enabled ? "scope_and_rewards" : "legacy";
|
|
588
|
+
const sourceCurrency = team.currency;
|
|
589
|
+
result.currency = typeof team.currency === "string" && team.currency.trim() ? team.currency : null;
|
|
590
|
+
if (result.currency === null)
|
|
591
|
+
issue("missing_currency", "Reward currency is unknown");
|
|
592
|
+
if (team.bounty_table === null) {
|
|
593
|
+
result.groups = [];
|
|
594
|
+
issue("no_published_table", "No public bounty table is available");
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
const tableId = team.bounty_table?._id;
|
|
598
|
+
if (typeof tableId !== "string" || !tableId)
|
|
599
|
+
throw new Error("Malformed bounty table");
|
|
600
|
+
result.description = typeof team.bounty_table.description === "string" ? team.bounty_table.description : null;
|
|
601
|
+
const cursors = new Set();
|
|
602
|
+
while (true) {
|
|
603
|
+
const table = team?.bounty_table;
|
|
604
|
+
if (table?._id !== tableId || team.handle !== result.handle ||
|
|
605
|
+
team.scope_and_rewards_groups_enabled !== (result.format === "scope_and_rewards") ||
|
|
606
|
+
team.currency !== sourceCurrency) {
|
|
607
|
+
throw new Error("Program rewards changed during pagination");
|
|
608
|
+
}
|
|
609
|
+
for (const row of nodes(table.bounty_table_rows)) {
|
|
610
|
+
if (typeof row._id !== "string" || !row._id || rows.has(row._id)) {
|
|
611
|
+
throw new Error("Missing or repeated reward group ID");
|
|
612
|
+
}
|
|
613
|
+
const useRange = typeof row.use_range === "boolean" ? row.use_range : null;
|
|
614
|
+
const rewards = {};
|
|
615
|
+
let incomplete = useRange === null;
|
|
616
|
+
for (const severity of ["low", "medium", "high", "critical"]) {
|
|
617
|
+
const amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
618
|
+
let maximum = amount(row[severity]);
|
|
619
|
+
let minimum = useRange === false ? maximum : amount(row[`${severity}_minimum`]);
|
|
620
|
+
if (minimum !== null && maximum !== null && minimum > maximum) {
|
|
621
|
+
minimum = maximum = null;
|
|
622
|
+
}
|
|
623
|
+
incomplete ||= minimum === null || maximum === null;
|
|
624
|
+
rewards[severity] = {
|
|
625
|
+
label: typeof table[`${severity}_label`] === "string" ? table[`${severity}_label`] : null,
|
|
626
|
+
minimum, maximum,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (incomplete)
|
|
630
|
+
issue("incomplete_rewards", "Some payout amounts, bounds, or the range setting are unknown or invalid", row._id);
|
|
631
|
+
groups.push({
|
|
632
|
+
id: row._id, name: typeof row.name === "string" ? row.name : null,
|
|
633
|
+
description: typeof row.description === "string" ? row.description : null,
|
|
634
|
+
scope_kind: result.format === "scope_and_rewards" ? "reward_group" :
|
|
635
|
+
row.structured_scope === null ? "all_assets" : isObject(row.structured_scope) ? "asset" : null,
|
|
636
|
+
use_range: useRange, rewards, assets: null, assets_availability: "unavailable",
|
|
637
|
+
});
|
|
638
|
+
rows.set(row._id, row);
|
|
639
|
+
}
|
|
640
|
+
const after = nextCursor(table.bounty_table_rows, cursors);
|
|
641
|
+
if (after === null)
|
|
642
|
+
break;
|
|
643
|
+
team = await request(rewardsQuery, { after });
|
|
644
|
+
}
|
|
645
|
+
if (!groups.length) {
|
|
646
|
+
result.groups = [];
|
|
647
|
+
issue("no_published_rewards", "The public bounty table contains no reward rows");
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
issue("upstream_error", `Could not retrieve every reward group: ${err.message}`);
|
|
653
|
+
if (!groups.length)
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
result.groups = groups;
|
|
657
|
+
let allAssets;
|
|
658
|
+
for (const group of groups) {
|
|
659
|
+
try {
|
|
660
|
+
const assets = [];
|
|
661
|
+
const seenIds = new Set();
|
|
662
|
+
const append = (rawAssets) => {
|
|
663
|
+
for (const raw of rawAssets) {
|
|
664
|
+
const item = asset(raw);
|
|
665
|
+
if (seenIds.has(item.id))
|
|
666
|
+
throw new Error("Repeated scope asset during pagination");
|
|
667
|
+
seenIds.add(item.id);
|
|
668
|
+
assets.push(item);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
if (group.scope_kind === "reward_group") {
|
|
672
|
+
const rowId = Number(group.id);
|
|
673
|
+
if (!/^\d+$/.test(group.id) || !Number.isSafeInteger(rowId) || rowId < 1 || rowId > 2147483647) {
|
|
674
|
+
throw new Error("Invalid reward group ID for scope search");
|
|
675
|
+
}
|
|
676
|
+
let total;
|
|
677
|
+
while (true) {
|
|
678
|
+
const team = await request(`query ProgramRewardAssets($handle: String!, $rowId: Int!, $from: Int!) {
|
|
679
|
+
team(handle: $handle) {
|
|
680
|
+
structured_scopes_search(bounty_table_row_id: $rowId, from: $from, size: 100,
|
|
681
|
+
sort: {field: "id", direction: ASC}) {
|
|
682
|
+
total_count edges { node { ... on StructuredScopeDocument {
|
|
683
|
+
_id asset_identifier: identifier asset_type eligible_for_submission eligible_for_bounty
|
|
684
|
+
} } }
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}`, { rowId, from: assets.length });
|
|
688
|
+
const page = team?.structured_scopes_search;
|
|
689
|
+
const batch = nodes(page);
|
|
690
|
+
if (!Number.isSafeInteger(page.total_count) || page.total_count < 0 ||
|
|
691
|
+
(total !== undefined && total !== page.total_count)) {
|
|
692
|
+
throw new Error("Missing or inconsistent reward asset total");
|
|
693
|
+
}
|
|
694
|
+
total = page.total_count;
|
|
695
|
+
append(batch);
|
|
696
|
+
if (assets.length === total)
|
|
697
|
+
break;
|
|
698
|
+
if (!batch.length || assets.length > total)
|
|
699
|
+
throw new Error("Incomplete reward asset search");
|
|
700
|
+
}
|
|
701
|
+
group.assets = assets;
|
|
702
|
+
}
|
|
703
|
+
else if (group.scope_kind === "asset") {
|
|
704
|
+
group.assets = [asset(rows.get(group.id).structured_scope)];
|
|
705
|
+
}
|
|
706
|
+
else if (group.scope_kind === "all_assets") {
|
|
707
|
+
if (!allAssets) {
|
|
708
|
+
let after = null;
|
|
709
|
+
const cursors = new Set();
|
|
710
|
+
do {
|
|
711
|
+
const team = await request(`query ProgramRewardDefaultAssets($handle: String!, $after: String) {
|
|
712
|
+
team(handle: $handle) {
|
|
713
|
+
structured_scopes(first: 100, after: $after, archived: false,
|
|
714
|
+
eligible_for_submission: true, eligible_for_bounty: true) {
|
|
715
|
+
pageInfo { hasNextPage endCursor }
|
|
716
|
+
edges { node { ${scopeFields} } }
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}`, { after });
|
|
720
|
+
const page = team?.structured_scopes;
|
|
721
|
+
append(nodes(page));
|
|
722
|
+
after = nextCursor(page, cursors);
|
|
723
|
+
} while (after !== null);
|
|
724
|
+
allAssets = assets;
|
|
725
|
+
}
|
|
726
|
+
group.assets = allAssets;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
throw new Error("Legacy reward scope applicability is unknown");
|
|
730
|
+
}
|
|
731
|
+
group.assets_availability = group.assets.some((a) => a.asset_type === null || a.eligible_for_bounty === null || a.eligible_for_submission === null) ? "partial" : "available";
|
|
732
|
+
if (group.assets_availability === "partial")
|
|
733
|
+
issue("incomplete_asset_fields", "Some asset types or eligibility fields are unknown", group.id);
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
issue("assets_unavailable", `Could not retrieve every asset: ${err.message}`, group.id);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
result.availability = result.issues.length ? "partial" : "available";
|
|
740
|
+
if (result.availability === "available")
|
|
741
|
+
cacheSet(cacheKey, result);
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
485
744
|
// ── Get program scope (auto-paginated) ────────────────────────────
|
|
486
745
|
export async function getProgramScope(handle, pageSize) {
|
|
487
746
|
const allData = await h1FetchAllPages(`/hackers/programs/${handle}/structured_scopes`, pageSize);
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { searchReports, getReport, getReportActivities, getReportSummary, listPrograms, getProgramDetails, getProgramSignalRequirements, getProgramScope, getProgramWeaknesses, getEarnings, getHackerProfile, getBalance, submitReport, addComment, closeReport, searchDisclosedReports, } from "./h1client.js";
|
|
5
|
+
import { searchReports, getReport, getReportActivities, getReportSummary, listPrograms, getProgramDetails, getProgramSignalRequirements, getProgramRewards, getProgramScope, getProgramWeaknesses, getEarnings, getHackerProfile, getBalance, submitReport, addComment, closeReport, searchDisclosedReports, } from "./h1client.js";
|
|
6
6
|
const server = new McpServer({
|
|
7
7
|
name: "hackerone",
|
|
8
8
|
version: "2.0.0",
|
|
@@ -204,7 +204,26 @@ server.tool("get_program_signal_requirements", "Get a public program's minimum p
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
|
-
// ── Tool:
|
|
207
|
+
// ── Tool: get_program_rewards ─────────────────────────────────────
|
|
208
|
+
server.tool("get_program_rewards", "Get a public program's published reward groups, currency, Low/Medium/High/Critical payout ranges, and associated scope assets. Supports legacy tables and Scope and rewards groups. Missing values are null, not zero; inspect availability and issues before relying on incomplete data. Legacy asset-specific rows take precedence over all-assets rows for the same asset ID. Private programs are not supported.", {
|
|
209
|
+
program_handle: z.string().trim().min(1).describe("Public program handle (e.g. 'security', 'tiktok')"),
|
|
210
|
+
}, { readOnlyHint: true }, async ({ program_handle }) => {
|
|
211
|
+
try {
|
|
212
|
+
const rewards = await getProgramRewards(program_handle);
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: JSON.stringify(rewards, null, 2) }],
|
|
215
|
+
isError: rewards.availability === "unavailable" &&
|
|
216
|
+
rewards.issues.some((issue) => issue.code === "upstream_error"),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// ── Tool: analyze_report_patterns ────────────────────────────────
|
|
208
227
|
server.tool("analyze_report_patterns", "Fetch your recent reports and analyze patterns: most common vulnerability types, severity distribution, resolution rates, and programs. Useful for understanding your hunting profile.", {
|
|
209
228
|
page_size: z
|
|
210
229
|
.number()
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hackerone-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for accessing HackerOne reports in Claude Code",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/11philip22/hackerone-mcp-server.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"main": "dist/index.js",
|
|
7
11
|
"bin": {
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
"scripts": {
|
|
14
18
|
"build": "tsc",
|
|
15
19
|
"prepack": "npm run build",
|
|
16
|
-
"test": "npm run build && node --test test/*.test.mjs",
|
|
20
|
+
"test": "npm run build && node --experimental-test-module-mocks --test test/*.test.mjs",
|
|
17
21
|
"start": "node dist/index.js",
|
|
18
22
|
"dev": "tsx src/index.ts"
|
|
19
23
|
},
|