bug-bounty-intelligence-mcp 1.0.5 → 1.2.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/index.mjs CHANGED
@@ -1,284 +1,289 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Bug Bounty Intelligence — MCP Server
4
- *
5
- * Tools:
6
- * scan_contract(repo_url, protocol_type?) → submit for analysis, returns job_id
7
- * get_scan_report(job_id) → poll status + get report URL
8
- * list_vulnerability_patterns(protocol?) → show historical patterns from 27K findings
9
- *
10
- * Payment: $5 USDC on Base (eip155:8453) via x402.
11
- * If you receive a payment_required response, pay to the address shown and retry.
12
- *
13
- * Run: node index.mjs (or: npx -y bug-bounty-intelligence-mcp@latest)
14
- */
15
-
16
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
17
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
18
- import {
19
- CallToolRequestSchema,
20
- ListToolsRequestSchema,
21
- } from '@modelcontextprotocol/sdk/types.js'
22
- const API_URL = 'https://x402.tokenizen.nl'
23
-
24
- // Embedded pattern library — Sherlock audit-competition findings, updated 2026-07-21.
25
- // Methodology: only contests where Sherlock's own judging-repo result structure could be
26
- // exactly reconciled (accepted + invalid === total submissions) are included — 10 contests,
27
- // 1,032 findings. A much larger raw corpus exists (27,681 submissions across 105 contests),
28
- // but acceptance could not be verified for the rest without risking a wrong number, so
29
- // those are excluded rather than estimated. See BENCHMARK.md for the full methodology note.
30
- const PATTERN_LIBRARY = {
31
- 'reentrancy': { acceptance_rate: '0.78', total: 51, accepted: 40, examples_accepted: ['m4k2 - Reentrancy in GoatPairV1::burn if token is a non-standard ERC20', 'Tonchi - Reentrancy in burn function, as it does not update the state of liquidity', 'C1rdan - hacker can steal fee from LPs'] },
32
- 'overflow': { acceptance_rate: '0.58', total: 45, accepted: 26, examples_accepted: ['Fassi_Security - When an order is exactly matched, a buyer can end up paying more', 'mstpr-brainbot - Pairs with MAX_FEE can revert due to rounding inconsistencies', 'irresponsible - Partition rounds up which can cause orders to be unfillable'] },
33
- 'trusted-actor': { acceptance_rate: '0.51', total: 311, accepted: 159, examples_accepted: ['LTDingZhen - Users can grief fillers by set malicious ValidationContract', 'mstpr-brainbot - Pairs with MAX_FEE can revert due to rounding inconsistencies', 'HSP - Attacker can submit malicious order and user may lose funds'] },
34
- 'fee-miscalculation': { acceptance_rate: '0.50', total: 109, accepted: 55, examples_accepted: ['WangAudit - [H] RubiconFeeController::getFeeOutputs incorrectly creates feeOut', 'turvec - The feeController differs from its specification', 'hals - IG contract can be DoS-ed from minting or burning options'] },
35
- 'staleness': { acceptance_rate: '0.49', total: 174, accepted: 86, examples_accepted: ['LTDingZhen - Users can grief fillers by set malicious ValidationContract', 'HSP - Attacker can submit malicious order and user may lose funds', 'pkqs90 - Orders with equal decayStartTime and decayEndTime benefit the filler'] },
36
- 'mev-slippage': { acceptance_rate: '0.49', total: 89, accepted: 44, examples_accepted: ['ge6a - DOS of IG mint/burn because of _deltaHedgePosition() revert', 'ge6a - Dos through large deposit', 'ge6a - Permanent Dos through trackVaultFee()'] },
37
- 'access-control': { acceptance_rate: '0.47', total: 74, accepted: 35, examples_accepted: ['skatas192 - The owner address has never been set which will cause the auth modifier', 'bearonbike - DVP _mint/_burn function could be DoS-ed by FeeManager', 'KingNFT - Attack on FeeManager.trackVaultFee() to make the IG contract'] },
38
- 'flash-loan': { acceptance_rate: '0.46', total: 13, accepted: 6, examples_accepted: ['cawfree - The invariant maxDeposit for a Vault can be exceeded', 'mgf15 - Use of slot0 to get sqrtPriceX96 can lead to price manipulation', 'bughuntoor - Any user can do a AAVE flashloan on behalf of FlashLoanAggregator'] },
39
- 'dos-griefing': { acceptance_rate: '0.41', total: 156, accepted: 64, examples_accepted: ['ni8mare - execute transactions can be reverted by a malicious user', 'LTDingZhen - Users can grief fillers by set malicious ValidationContract', 'mstpr-brainbot - Pairs with MAX_FEE can revert due to rounding inconsistencies'] },
40
- 'rounding': { acceptance_rate: '0.40', total: 63, accepted: 25, examples_accepted: ['mstpr-brainbot - Pairs with MAX_FEE can revert due to rounding inconsistencies', 'KingNFT - Execution of orders would revert unexpectedly', 'blutorque - PartialFillLib::partition() unexpectedly reverts'] },
41
- 'oracle-manipulation':{ acceptance_rate: '0.36', total: 131, accepted: 47, examples_accepted: ['ge6a - DOS of IG mint/burn because of _deltaHedgePosition() revert', 'ge6a - Dos through large deposit', 'jasonxiale - Vault._state.liquidity.totalDeposit can avoid being decreased'] },
42
- 'liquidation': { acceptance_rate: '0.29', total: 14, accepted: 4, examples_accepted: ['M3azad - No updation of _state.liquidity.pendingWithdrawals may lead to abnormal', 'bughuntoor - Liquidation bonus scales exponentially instead of linearly', 'cawfree - Calls to addToBlackList(address,address[]) can be frontrun'] },
43
- }
44
-
45
- // ── helpers ──────────────────────────────────────────────────────────────────
46
-
47
- async function callApi(path, method = 'GET', body = null, extraHeaders = {}) {
48
- const opts = {
49
- method,
50
- headers: { 'Content-Type': 'application/json', ...extraHeaders },
51
- signal: AbortSignal.timeout(30_000),
52
- }
53
- if (body) opts.body = JSON.stringify(body)
54
- try {
55
- const res = await fetch(`${API_URL}${path}`, opts)
56
- const text = await res.text()
57
- let json
58
- try { json = JSON.parse(text) } catch { json = { raw: text } }
59
- return { status: res.status, ok: res.ok, json }
60
- } catch (err) {
61
- const reason = err.name === 'TimeoutError' || err.name === 'AbortError' ? 'timed out after 30s' : err.message
62
- return { status: 0, ok: false, json: { error: `Could not reach ${API_URL} (${reason})` } }
63
- }
64
- }
65
-
66
- function ilmPatterns(protocolType) {
67
- const TYPE = {
68
- DEX: ['oracle-manipulation','mev-slippage','rounding','flash-loan','fee-miscalculation','reentrancy'],
69
- LENDING: ['oracle-manipulation','liquidation','staleness','rounding','flash-loan','access-control'],
70
- BRIDGE: ['access-control','reentrancy','overflow','dos-griefing'],
71
- GOVERNANCE: ['access-control','dos-griefing','trusted-actor'],
72
- STAKING: ['rounding','fee-miscalculation','access-control','overflow','staleness'],
73
- GENERAL: ['oracle-manipulation','rounding','access-control','fee-miscalculation','reentrancy','flash-loan'],
74
- }
75
- const keys = TYPE[protocolType?.toUpperCase()] ?? TYPE.GENERAL
76
- return keys.filter(k => PATTERN_LIBRARY[k]).map(k => ({
77
- pattern: k,
78
- acceptance_rate: PATTERN_LIBRARY[k].acceptance_rate,
79
- total_cases: PATTERN_LIBRARY[k].total,
80
- example: (PATTERN_LIBRARY[k].examples_accepted ?? [])[0]?.slice(0, 120),
81
- }))
82
- }
83
-
84
- // ── MCP server ────────────────────────────────────────────────────────────────
85
-
86
- const server = new Server(
87
- { name: 'bug-bounty-intelligence', version: '1.0.0' },
88
- { capabilities: { tools: {} } }
89
- )
90
-
91
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
92
- tools: [
93
- {
94
- name: 'scan_contract',
95
- description: [
96
- 'Submit a public GitHub repository for an automated Solidity analysis. The result is a list of AI-generated hypotheses to review by hand. It is not an audit and not a list of confirmed bugs.',
97
- 'The scan reads the files most likely to move funds first, in at most six parts. Tests, mocks, scripts, interfaces and vendored dependencies are left out. The report states what share of the in-scope code was actually read, and a report can say that no hypotheses came out of the part that was read.',
98
- 'Check the repository first, for free: GET https://x402.tokenizen.nl/api/bug-intel/precheck?repo=owner/name says whether the scan counts any Solidity in it and roughly how much of it will be read.',
99
- 'How often the scan finds known issues on closed audit contests, including the misses, is published in the README of the repository (github.com/holistis/bug-bounty-intelligence-mcp). The prompt uses acceptance-rate hints from the 1,032 findings that could be exact-reconciled against contest outcomes, see METHODOLOGY.md.',
100
- 'Cost: $5 USDC on Base (eip155:8453) via x402. Reports usually arrive within the hour. If the scan fails or the report does not help, write to info@holistischadviseur.nl for a new attempt or a refund, see the terms.',
101
- 'Returns a job_id. Use get_scan_report to poll for results.',
102
- 'If payment_required is true, pay $5 USDC to the payTo address on Base, then retry scan_contract with the same repo_url.',
103
- ].join(' '),
104
- inputSchema: {
105
- type: 'object',
106
- properties: {
107
- repo_url: {
108
- type: 'string',
109
- description: 'Public GitHub repo URL containing Solidity contracts (e.g. https://github.com/org/repo)',
110
- },
111
- protocol_type: {
112
- type: 'string',
113
- enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
114
- description: 'Protocol category for targeted analysis (optional, default GENERAL)',
115
- },
116
- },
117
- required: ['repo_url'],
118
- },
119
- },
120
- {
121
- name: 'get_scan_report',
122
- description: [
123
- 'Poll the status of a job_id returned by scan_contract.',
124
- 'Call this every few minutes after submitting. Reports usually arrive within the hour; polling faster just re-checks a job that is still processing.',
125
- 'Returns status (queued/processing/complete/failed), the repo under scan, and once complete the reportUrl (the report of hypotheses, with the share of code that was read), the outcome and the coverage. A failed job says why and where to write for a new attempt or a refund.',
126
- 'A 404 means the job_id does not exist — double-check what scan_contract actually returned rather than retrying blindly.',
127
- ].join(' '),
128
- inputSchema: {
129
- type: 'object',
130
- properties: {
131
- job_id: { type: 'string', description: 'The job_id returned by scan_contract' },
132
- },
133
- required: ['job_id'],
134
- },
135
- },
136
- {
137
- name: 'list_vulnerability_patterns',
138
- description: [
139
- 'Returns historical vulnerability patterns and acceptance rates from Sherlock audit-competition findings.',
140
- 'Numbers are limited to contests where results could be exactly reconciled against Sherlock\'s own published outcomes (1,032 findings across 10 contests) — no estimated or unverifiable figures.',
141
- 'Useful for understanding what types of bugs are most likely to be valid in a given protocol type.',
142
- 'This tool is FREE — no payment required.',
143
- ].join(' '),
144
- inputSchema: {
145
- type: 'object',
146
- properties: {
147
- protocol_type: {
148
- type: 'string',
149
- enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
150
- description: 'Protocol type to filter patterns (default GENERAL)',
151
- },
152
- },
153
- },
154
- },
155
- ],
156
- }))
157
-
158
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
159
- const { name, arguments: args } = req.params
160
-
161
- // ── scan_contract ──────────────────────────────────────────────────────────
162
- if (name === 'scan_contract') {
163
- const { repo_url, protocol_type = 'GENERAL' } = args ?? {}
164
- if (!repo_url) {
165
- return { content: [{ type: 'text', text: 'Error: repo_url is required' }], isError: true }
166
- }
167
-
168
- const { status, json } = await callApi('/api/bug-intel', 'POST', { repo: repo_url, protocolType: protocol_type })
169
-
170
- if (status === 402) {
171
- const accept = json.accepts?.[0] ?? {}
172
- const amtUsdc = accept.amount ? (parseInt(accept.amount) / 1_000_000).toFixed(2) : '5.00'
173
- if (!accept.payTo || !accept.asset) {
174
- // Never fall back to a hardcoded address here: a stale one silently
175
- // sends real USDC to a wallet nobody is watching. Fail loud instead.
176
- return {
177
- content: [{
178
- type: 'text',
179
- text: 'PAYMENT REQUIRED, but the server did not return a payment address. Do not guess one. Retry scan_contract; if this repeats, the payment endpoint is broken, not the address.',
180
- }],
181
- isError: true,
182
- }
183
- }
184
- return {
185
- content: [{
186
- type: 'text',
187
- text: [
188
- 'PAYMENT REQUIRED to start the scan.',
189
- '',
190
- `Amount: ${amtUsdc} USDC`,
191
- `Network: Base mainnet (eip155:8453)`,
192
- `Pay to: ${accept.payTo}`,
193
- `Asset: USDC — ${accept.asset}`,
194
- '',
195
- 'After paying, retry scan_contract with the same repo_url.',
196
- 'Terms: https://x402.tokenizen.nl/terms',
197
- ].join('\n'),
198
- }],
199
- isError: false,
200
- }
201
- }
202
-
203
- if (status === 200 || status === 201) {
204
- return {
205
- content: [{
206
- type: 'text',
207
- text: [
208
- `Scan submitted. Job ID: ${json.jobId}`,
209
- `Status: ${json.status ?? 'queued'}`,
210
- '',
211
- `Poll for results: get_scan_report({ job_id: "${json.jobId}" })`,
212
- `Reports usually arrive within the hour. The report states how much of the code was read.`,
213
- ].join('\n'),
214
- }],
215
- }
216
- }
217
-
218
- return {
219
- content: [{ type: 'text', text: `API error ${status}: ${JSON.stringify(json).slice(0, 300)}` }],
220
- isError: true,
221
- }
222
- }
223
-
224
- // ── get_scan_report ────────────────────────────────────────────────────────
225
- if (name === 'get_scan_report') {
226
- const { job_id } = args ?? {}
227
- if (!job_id) {
228
- return { content: [{ type: 'text', text: 'Error: job_id is required' }], isError: true }
229
- }
230
-
231
- const { status, ok, json } = await callApi(`/api/bug-intel/${encodeURIComponent(job_id)}`)
232
-
233
- if (status === 404) {
234
- return { content: [{ type: 'text', text: `Job ${job_id} not found. Check the job_id.` }], isError: true }
235
- }
236
-
237
- if (!ok || !json.status) {
238
- return {
239
- content: [{ type: 'text', text: `Could not get status for job ${job_id}: ${json.error ?? `API error ${status}`}. Try again shortly.` }],
240
- isError: true,
241
- }
242
- }
243
-
244
- const lines = [`Job ID: ${job_id}`, `Status: ${json.status}`]
245
- if (json.repo) lines.push(`Repo: ${json.repo}`)
246
- if (json.reportUrl) lines.push(`Report: ${json.reportUrl}`)
247
- if (json.findingsCount !== undefined) lines.push(`Findings: ${json.findingsCount}`)
248
- if (json.status === 'queued' || json.status === 'processing') {
249
- lines.push('', 'Analysis in progress. Try again in a few minutes.')
250
- }
251
-
252
- return { content: [{ type: 'text', text: lines.join('\n') }] }
253
- }
254
-
255
- // ── list_vulnerability_patterns ───────────────────────────────────────────
256
- if (name === 'list_vulnerability_patterns') {
257
- const { protocol_type = 'GENERAL' } = args ?? {}
258
- const patterns = ilmPatterns(protocol_type)
259
-
260
- if (!patterns || patterns.length === 0) {
261
- return {
262
- content: [{ type: 'text', text: 'No patterns found for this protocol type. Try GENERAL.' }],
263
- }
264
- }
265
-
266
- const lines = [`Vulnerability patterns for ${protocol_type} protocols (from Sherlock audit findings, exact-reconciled contests only):`, '']
267
- for (const p of patterns) {
268
- const pct = Math.round(parseFloat(p.acceptance_rate) * 100)
269
- lines.push(`${p.pattern.padEnd(24)} ${pct}% accepted (${p.total_cases} cases)`)
270
- if (p.example) lines.push(` Example: ${p.example}`)
271
- }
272
- lines.push('', 'Run scan_contract to check your contracts against these patterns.')
273
-
274
- return { content: [{ type: 'text', text: lines.join('\n') }] }
275
- }
276
-
277
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
278
- })
279
-
280
- // ── start ─────────────────────────────────────────────────────────────────────
281
-
282
- const transport = new StdioServerTransport()
283
- await server.connect(transport)
284
- // server runs until process exits
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bug Bounty Intelligence — MCP Server
4
+ *
5
+ * Tools:
6
+ * scan_contract(repo_url, protocol_type?) → submit for analysis, returns job_id
7
+ * get_scan_report(job_id) → poll status + get report URL
8
+ * list_vulnerability_patterns(protocol?) → show historical patterns from 27K findings
9
+ *
10
+ * Payment: $5 USDC on Base (eip155:8453) via x402.
11
+ * If you receive a payment_required response, pay to the address shown and retry.
12
+ *
13
+ * Run: node index.mjs (or: npx -y bug-bounty-intelligence-mcp@latest)
14
+ */
15
+
16
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
17
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
18
+ import {
19
+ CallToolRequestSchema,
20
+ ListToolsRequestSchema,
21
+ } from '@modelcontextprotocol/sdk/types.js'
22
+ const API_URL = 'https://x402.tokenizen.nl'
23
+
24
+ // Embedded pattern library — Sherlock + Code4rena audit-competition findings, updated 2026-11-12.
25
+ // Methodology: only contests where the judging repo's own result structure could be exactly
26
+ // reconciled (accepted + invalid === total submissions) are included — 35 Sherlock contests
27
+ // (7,388 findings) plus 3 Code4rena contests (1,812 findings) that happen to reconcile under
28
+ // the same zero-tolerance rule, 38 contests, 9,200 findings total. Code4rena contributes far
29
+ // less than Sherlock because most of its history does not use a consistently-applied
30
+ // satisfactory/unsatisfactory label (verified by sampling, not assumed), so those contests are
31
+ // excluded rather than estimated, exactly like the excluded Sherlock contests. See
32
+ // METHODOLOGY.md for the full methodology note, including the 2026-11-12 re-verification that
33
+ // replaced an unreliable local cache.
34
+ const PATTERN_LIBRARY = {
35
+ 'flash-loan': { acceptance_rate: '0.66', total: 193, accepted: 127, examples_accepted: ['duc - Borrower can reduce the interest of shortly loans', 'ShadowForce - Bypass the VAULT_ACCOUNT_MIN_TIME check in VaultAccountAction.sol when exiting the vau', 'mstpr-brainbot - Compound exchange rate can be manipulated to withdraw more underlying tokens from N'] },
36
+ 'liquidation': { acceptance_rate: '0.66', total: 877, accepted: 576, examples_accepted: ['w42d3n - The protocol is not protected against Signature malleability', 'gryphon - Buyer may not be able to liquidate the loan', 'peanuts - NFTs are not stored in the appropriate contracts'] },
37
+ 'fee-miscalculation': { acceptance_rate: '0.54', total: 583, accepted: 313, examples_accepted: ['7siech - Total loss of funds when using certain popular ERC20 tokens', 'iglyx - MigratePrimeCash\'s _calculateInterestRateCurves calculates new kinkRate1 and kinkRate2 incor', 'xiaoming90 - Prime vaults do not consider secondary debt when assessing vault fees'] },
38
+ 'oracle-manipulation':{ acceptance_rate: '0.53', total: 949, accepted: 502, examples_accepted: ['iglyx - TokenHandler\'s _redeemMoneyMarketIfRequired can fail valid withdrawal requests', 'xiaoming90 - Underlying delta is calculated on internal token balance', 'xiaoming90 - Rebalance stops working when more holdings are added'] },
39
+ 'staleness': { acceptance_rate: '0.51', total: 1315, accepted: 675, examples_accepted: ['w42d3n - The protocol is not protected against Signature malleability', 'peanuts - Some NFTs like CryptoKitty and CryptoFighter can be paused, which block repaying/liquidati', 'csanuragjain - Liquidation occur at incorrect time'] },
40
+ 'mev-slippage': { acceptance_rate: '0.47', total: 379, accepted: 179, examples_accepted: ['xiaoming90 - Liquidation discount does not take slippage into consideration', 'xiaoming90 - Inadequate slippage control', 'ShadowForce - Bypass the VAULT_ACCOUNT_MIN_TIME check in VaultAccountAction.sol when exiting the vau'] },
41
+ 'trusted-actor': { acceptance_rate: '0.47', total: 2127, accepted: 1000, examples_accepted: ['gryphon - Buyer may not be able to liquidate the loan', 'peanuts - Some NFTs like CryptoKitty and CryptoFighter can be paused, which block repaying/liquidati', 'Go-langer - Init function left open for anyone to call and initialize the protocol.'] },
42
+ 'dos-griefing': { acceptance_rate: '0.47', total: 816, accepted: 383, examples_accepted: ['w42d3n - The protocol is not protected against Signature malleability', 'duc - Borrower can reduce the interest of shortly loans', '0xd1r4cde17a - lender gets minInterestsToRepay / loan.nbOfPositions'] },
43
+ 'overflow': { acceptance_rate: '0.43', total: 385, accepted: 167, examples_accepted: ['peanuts - Some NFTs like CryptoKitty and CryptoFighter can be paused, which block repaying/liquidati', 'Robert - checkedTransfer In claim() and claimAsBorrower() Functions Can Permanently Lock Tokens', 'Inspex - Denial-of-Service in the liquidation flow results in the collateral NTF will be stuck in th'] },
44
+ 'access-control': { acceptance_rate: '0.42', total: 846, accepted: 356, examples_accepted: ['Go-langer - Init function left open for anyone to call and initialize the protocol.', 'Englave - Signature Replay Attack on OfferArgs', 'xiaoming90 - It may be possible to liquidate on behalf of another account'] },
45
+ 'rounding': { acceptance_rate: '0.39', total: 532, accepted: 205, examples_accepted: ['usmannk - Borrowers may steal funds from lenders', 'jekapi - Borrower can lock lender assets if offer has large LTV', 'iglyx - MigratePrimeCash\'s _calculateInterestRateCurves calculates new kinkRate1 and kinkRate2 incor'] },
46
+ 'reentrancy': { acceptance_rate: '0.34', total: 332, accepted: 114, examples_accepted: ['SPYBOY - A malicious signer can exploite reentrancy while Borrowing', 'glcanvas - Possible reentrancy which allows to write incorrect supplyPositionIndex', 'Robert - A Loan\'s supplyPositionIndex Can Be Duplicated Through Re-entrancy'] },
47
+ 'idor': { acceptance_rate: '0.26', total: 35, accepted: 9, examples_accepted: ['0Kage - A borrower with multiple loans of which one is liquidated can withdraw much more than his sh', '0xbepresent - Claimable amount could be sent to the approved address instead the provision/position', 'GimelSec - ClaimFacet.sendShareOfSaleAsSupplier() calculates unfair shares, the money should compe'] },
48
+ }
49
+
50
+ // ── helpers ──────────────────────────────────────────────────────────────────
51
+
52
+ async function callApi(path, method = 'GET', body = null, extraHeaders = {}) {
53
+ const opts = {
54
+ method,
55
+ headers: { 'Content-Type': 'application/json', ...extraHeaders },
56
+ signal: AbortSignal.timeout(30_000),
57
+ }
58
+ if (body) opts.body = JSON.stringify(body)
59
+ try {
60
+ const res = await fetch(`${API_URL}${path}`, opts)
61
+ const text = await res.text()
62
+ let json
63
+ try { json = JSON.parse(text) } catch { json = { raw: text } }
64
+ return { status: res.status, ok: res.ok, json }
65
+ } catch (err) {
66
+ const reason = err.name === 'TimeoutError' || err.name === 'AbortError' ? 'timed out after 30s' : err.message
67
+ return { status: 0, ok: false, json: { error: `Could not reach ${API_URL} (${reason})` } }
68
+ }
69
+ }
70
+
71
+ function ilmPatterns(protocolType) {
72
+ const TYPE = {
73
+ DEX: ['oracle-manipulation','mev-slippage','rounding','flash-loan','fee-miscalculation','reentrancy'],
74
+ LENDING: ['oracle-manipulation','liquidation','staleness','rounding','flash-loan','access-control'],
75
+ BRIDGE: ['access-control','reentrancy','overflow','dos-griefing'],
76
+ GOVERNANCE: ['access-control','dos-griefing','trusted-actor'],
77
+ STAKING: ['rounding','fee-miscalculation','access-control','overflow','staleness'],
78
+ GENERAL: ['oracle-manipulation','rounding','access-control','fee-miscalculation','reentrancy','flash-loan'],
79
+ }
80
+ const keys = TYPE[protocolType?.toUpperCase()] ?? TYPE.GENERAL
81
+ return keys.filter(k => PATTERN_LIBRARY[k]).map(k => ({
82
+ pattern: k,
83
+ acceptance_rate: PATTERN_LIBRARY[k].acceptance_rate,
84
+ total_cases: PATTERN_LIBRARY[k].total,
85
+ example: (PATTERN_LIBRARY[k].examples_accepted ?? [])[0]?.slice(0, 120),
86
+ }))
87
+ }
88
+
89
+ // ── MCP server ────────────────────────────────────────────────────────────────
90
+
91
+ const server = new Server(
92
+ { name: 'bug-bounty-intelligence', version: '1.0.0' },
93
+ { capabilities: { tools: {} } }
94
+ )
95
+
96
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
97
+ tools: [
98
+ {
99
+ name: 'scan_contract',
100
+ description: [
101
+ 'Submit a public GitHub repository for an automated Solidity analysis. The result is a list of AI-generated hypotheses to review by hand. It is not an audit and not a list of confirmed bugs.',
102
+ 'The scan reads the files most likely to move funds first, in at most six parts. Tests, mocks, scripts, interfaces and vendored dependencies are left out. The report states what share of the in-scope code was actually read, and a report can say that no hypotheses came out of the part that was read.',
103
+ 'Check the repository first, for free: GET https://x402.tokenizen.nl/api/bug-intel/precheck?repo=owner/name says whether the scan counts any Solidity in it and roughly how much of it will be read.',
104
+ 'How often the scan finds known issues on closed audit contests, including the misses, is published in the README of the repository (github.com/holistis/bug-bounty-intelligence-mcp). The prompt uses acceptance-rate hints from the 9,200 findings that could be exact-reconciled against contest outcomes, see METHODOLOGY.md.',
105
+ 'Cost: $5 USDC on Base (eip155:8453) via x402. Reports usually arrive within the hour. If the scan fails or the report does not help, write to info@holistischadviseur.nl for a new attempt or a refund, see the terms.',
106
+ 'Returns a job_id. Use get_scan_report to poll for results.',
107
+ 'If payment_required is true, pay $5 USDC to the payTo address on Base, then retry scan_contract with the same repo_url.',
108
+ ].join(' '),
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {
112
+ repo_url: {
113
+ type: 'string',
114
+ description: 'Public GitHub repo URL containing Solidity contracts (e.g. https://github.com/org/repo)',
115
+ },
116
+ protocol_type: {
117
+ type: 'string',
118
+ enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
119
+ description: 'Protocol category for targeted analysis (optional, default GENERAL)',
120
+ },
121
+ },
122
+ required: ['repo_url'],
123
+ },
124
+ },
125
+ {
126
+ name: 'get_scan_report',
127
+ description: [
128
+ 'Poll the status of a job_id returned by scan_contract.',
129
+ 'Call this every few minutes after submitting. Reports usually arrive within the hour; polling faster just re-checks a job that is still processing.',
130
+ 'Returns status (queued/processing/complete/failed), the repo under scan, and once complete the reportUrl (the report of hypotheses, with the share of code that was read), the outcome and the coverage. A failed job says why and where to write for a new attempt or a refund.',
131
+ 'A 404 means the job_id does not exist — double-check what scan_contract actually returned rather than retrying blindly.',
132
+ ].join(' '),
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ job_id: { type: 'string', description: 'The job_id returned by scan_contract' },
137
+ },
138
+ required: ['job_id'],
139
+ },
140
+ },
141
+ {
142
+ name: 'list_vulnerability_patterns',
143
+ description: [
144
+ 'Returns historical vulnerability patterns and acceptance rates from Sherlock and Code4rena audit-competition findings.',
145
+ 'Numbers are limited to contests where results could be exactly reconciled against the platform\'s own published outcomes (9,200 findings across 38 contests: 35 Sherlock, 3 Code4rena), no estimated or unverifiable figures.',
146
+ 'Useful for understanding what types of bugs are most likely to be valid in a given protocol type.',
147
+ 'This tool is FREE — no payment required.',
148
+ ].join(' '),
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ protocol_type: {
153
+ type: 'string',
154
+ enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
155
+ description: 'Protocol type to filter patterns (default GENERAL)',
156
+ },
157
+ },
158
+ },
159
+ },
160
+ ],
161
+ }))
162
+
163
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
164
+ const { name, arguments: args } = req.params
165
+
166
+ // ── scan_contract ──────────────────────────────────────────────────────────
167
+ if (name === 'scan_contract') {
168
+ const { repo_url, protocol_type = 'GENERAL' } = args ?? {}
169
+ if (!repo_url) {
170
+ return { content: [{ type: 'text', text: 'Error: repo_url is required' }], isError: true }
171
+ }
172
+
173
+ const { status, json } = await callApi('/api/bug-intel', 'POST', { repo: repo_url, protocolType: protocol_type })
174
+
175
+ if (status === 402) {
176
+ const accept = json.accepts?.[0] ?? {}
177
+ const amtUsdc = accept.amount ? (parseInt(accept.amount) / 1_000_000).toFixed(2) : '5.00'
178
+ if (!accept.payTo || !accept.asset) {
179
+ // Never fall back to a hardcoded address here: a stale one silently
180
+ // sends real USDC to a wallet nobody is watching. Fail loud instead.
181
+ return {
182
+ content: [{
183
+ type: 'text',
184
+ text: 'PAYMENT REQUIRED, but the server did not return a payment address. Do not guess one. Retry scan_contract; if this repeats, the payment endpoint is broken, not the address.',
185
+ }],
186
+ isError: true,
187
+ }
188
+ }
189
+ return {
190
+ content: [{
191
+ type: 'text',
192
+ text: [
193
+ 'PAYMENT REQUIRED to start the scan.',
194
+ '',
195
+ `Amount: ${amtUsdc} USDC`,
196
+ `Network: Base mainnet (eip155:8453)`,
197
+ `Pay to: ${accept.payTo}`,
198
+ `Asset: USDC — ${accept.asset}`,
199
+ '',
200
+ 'After paying, retry scan_contract with the same repo_url.',
201
+ 'Terms: https://x402.tokenizen.nl/terms',
202
+ ].join('\n'),
203
+ }],
204
+ isError: false,
205
+ }
206
+ }
207
+
208
+ if (status === 200 || status === 201) {
209
+ return {
210
+ content: [{
211
+ type: 'text',
212
+ text: [
213
+ `Scan submitted. Job ID: ${json.jobId}`,
214
+ `Status: ${json.status ?? 'queued'}`,
215
+ '',
216
+ `Poll for results: get_scan_report({ job_id: "${json.jobId}" })`,
217
+ `Reports usually arrive within the hour. The report states how much of the code was read.`,
218
+ ].join('\n'),
219
+ }],
220
+ }
221
+ }
222
+
223
+ return {
224
+ content: [{ type: 'text', text: `API error ${status}: ${JSON.stringify(json).slice(0, 300)}` }],
225
+ isError: true,
226
+ }
227
+ }
228
+
229
+ // ── get_scan_report ────────────────────────────────────────────────────────
230
+ if (name === 'get_scan_report') {
231
+ const { job_id } = args ?? {}
232
+ if (!job_id) {
233
+ return { content: [{ type: 'text', text: 'Error: job_id is required' }], isError: true }
234
+ }
235
+
236
+ const { status, ok, json } = await callApi(`/api/bug-intel/${encodeURIComponent(job_id)}`)
237
+
238
+ if (status === 404) {
239
+ return { content: [{ type: 'text', text: `Job ${job_id} not found. Check the job_id.` }], isError: true }
240
+ }
241
+
242
+ if (!ok || !json.status) {
243
+ return {
244
+ content: [{ type: 'text', text: `Could not get status for job ${job_id}: ${json.error ?? `API error ${status}`}. Try again shortly.` }],
245
+ isError: true,
246
+ }
247
+ }
248
+
249
+ const lines = [`Job ID: ${job_id}`, `Status: ${json.status}`]
250
+ if (json.repo) lines.push(`Repo: ${json.repo}`)
251
+ if (json.reportUrl) lines.push(`Report: ${json.reportUrl}`)
252
+ if (json.findingsCount !== undefined) lines.push(`Findings: ${json.findingsCount}`)
253
+ if (json.status === 'queued' || json.status === 'processing') {
254
+ lines.push('', 'Analysis in progress. Try again in a few minutes.')
255
+ }
256
+
257
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
258
+ }
259
+
260
+ // ── list_vulnerability_patterns ───────────────────────────────────────────
261
+ if (name === 'list_vulnerability_patterns') {
262
+ const { protocol_type = 'GENERAL' } = args ?? {}
263
+ const patterns = ilmPatterns(protocol_type)
264
+
265
+ if (!patterns || patterns.length === 0) {
266
+ return {
267
+ content: [{ type: 'text', text: 'No patterns found for this protocol type. Try GENERAL.' }],
268
+ }
269
+ }
270
+
271
+ const lines = [`Vulnerability patterns for ${protocol_type} protocols (from Sherlock and Code4rena audit findings, exact-reconciled contests only):`, '']
272
+ for (const p of patterns) {
273
+ const pct = Math.round(parseFloat(p.acceptance_rate) * 100)
274
+ lines.push(`${p.pattern.padEnd(24)} ${pct}% accepted (${p.total_cases} cases)`)
275
+ if (p.example) lines.push(` Example: ${p.example}`)
276
+ }
277
+ lines.push('', 'Run scan_contract to check your contracts against these patterns.')
278
+
279
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
280
+ }
281
+
282
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
283
+ })
284
+
285
+ // ── start ─────────────────────────────────────────────────────────────────────
286
+
287
+ const transport = new StdioServerTransport()
288
+ await server.connect(transport)
289
+ // server runs until process exits