bug-bounty-intelligence-mcp 1.0.5 → 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/index.mjs CHANGED
@@ -1,284 +1,286 @@
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 audit-competition findings, updated 2026-11-12.
25
+ // Methodology: only contests where Sherlock's own judging-repo result structure could be
26
+ // exactly reconciled (accepted + invalid === total submissions) are included, 35 contests,
27
+ // 7,388 findings. A much larger raw corpus exists (56,657 submissions across 212 measured
28
+ // contests), but acceptance could not be verified for the rest without risking a wrong number,
29
+ // so those are excluded rather than estimated. See METHODOLOGY.md for the full methodology note,
30
+ // including the 2026-11-12 re-verification that replaced an unreliable local cache.
31
+ const PATTERN_LIBRARY = {
32
+ 'flash-loan': { acceptance_rate: '0.69', total: 185, 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'] },
33
+ 'liquidation': { acceptance_rate: '0.66', total: 869, 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'] },
34
+ 'fee-miscalculation': { acceptance_rate: '0.55', total: 540, accepted: 297, 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'] },
35
+ 'dos-griefing': { acceptance_rate: '0.54', total: 680, accepted: 365, 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'] },
36
+ 'staleness': { acceptance_rate: '0.54', total: 1090, accepted: 586, 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'] },
37
+ 'oracle-manipulation':{ acceptance_rate: '0.54', total: 911, accepted: 488, 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'] },
38
+ 'trusted-actor': { acceptance_rate: '0.52', total: 1814, accepted: 935, 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.'] },
39
+ 'idor': { acceptance_rate: '0.50', total: 18, 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'] },
40
+ 'overflow': { acceptance_rate: '0.48', total: 346, accepted: 165, 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'] },
41
+ 'mev-slippage': { acceptance_rate: '0.48', total: 368, accepted: 178, 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'] },
42
+ 'access-control': { acceptance_rate: '0.46', total: 538, accepted: 249, 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'] },
43
+ 'reentrancy': { acceptance_rate: '0.40', total: 279, accepted: 111, 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'] },
44
+ 'rounding': { acceptance_rate: '0.40', total: 485, accepted: 192, 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'] },
45
+ }
46
+
47
+ // ── helpers ──────────────────────────────────────────────────────────────────
48
+
49
+ async function callApi(path, method = 'GET', body = null, extraHeaders = {}) {
50
+ const opts = {
51
+ method,
52
+ headers: { 'Content-Type': 'application/json', ...extraHeaders },
53
+ signal: AbortSignal.timeout(30_000),
54
+ }
55
+ if (body) opts.body = JSON.stringify(body)
56
+ try {
57
+ const res = await fetch(`${API_URL}${path}`, opts)
58
+ const text = await res.text()
59
+ let json
60
+ try { json = JSON.parse(text) } catch { json = { raw: text } }
61
+ return { status: res.status, ok: res.ok, json }
62
+ } catch (err) {
63
+ const reason = err.name === 'TimeoutError' || err.name === 'AbortError' ? 'timed out after 30s' : err.message
64
+ return { status: 0, ok: false, json: { error: `Could not reach ${API_URL} (${reason})` } }
65
+ }
66
+ }
67
+
68
+ function ilmPatterns(protocolType) {
69
+ const TYPE = {
70
+ DEX: ['oracle-manipulation','mev-slippage','rounding','flash-loan','fee-miscalculation','reentrancy'],
71
+ LENDING: ['oracle-manipulation','liquidation','staleness','rounding','flash-loan','access-control'],
72
+ BRIDGE: ['access-control','reentrancy','overflow','dos-griefing'],
73
+ GOVERNANCE: ['access-control','dos-griefing','trusted-actor'],
74
+ STAKING: ['rounding','fee-miscalculation','access-control','overflow','staleness'],
75
+ GENERAL: ['oracle-manipulation','rounding','access-control','fee-miscalculation','reentrancy','flash-loan'],
76
+ }
77
+ const keys = TYPE[protocolType?.toUpperCase()] ?? TYPE.GENERAL
78
+ return keys.filter(k => PATTERN_LIBRARY[k]).map(k => ({
79
+ pattern: k,
80
+ acceptance_rate: PATTERN_LIBRARY[k].acceptance_rate,
81
+ total_cases: PATTERN_LIBRARY[k].total,
82
+ example: (PATTERN_LIBRARY[k].examples_accepted ?? [])[0]?.slice(0, 120),
83
+ }))
84
+ }
85
+
86
+ // ── MCP server ────────────────────────────────────────────────────────────────
87
+
88
+ const server = new Server(
89
+ { name: 'bug-bounty-intelligence', version: '1.0.0' },
90
+ { capabilities: { tools: {} } }
91
+ )
92
+
93
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
94
+ tools: [
95
+ {
96
+ name: 'scan_contract',
97
+ description: [
98
+ '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.',
99
+ '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.',
100
+ '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.',
101
+ '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 7,388 findings that could be exact-reconciled against contest outcomes, see METHODOLOGY.md.',
102
+ '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.',
103
+ 'Returns a job_id. Use get_scan_report to poll for results.',
104
+ 'If payment_required is true, pay $5 USDC to the payTo address on Base, then retry scan_contract with the same repo_url.',
105
+ ].join(' '),
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ repo_url: {
110
+ type: 'string',
111
+ description: 'Public GitHub repo URL containing Solidity contracts (e.g. https://github.com/org/repo)',
112
+ },
113
+ protocol_type: {
114
+ type: 'string',
115
+ enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
116
+ description: 'Protocol category for targeted analysis (optional, default GENERAL)',
117
+ },
118
+ },
119
+ required: ['repo_url'],
120
+ },
121
+ },
122
+ {
123
+ name: 'get_scan_report',
124
+ description: [
125
+ 'Poll the status of a job_id returned by scan_contract.',
126
+ 'Call this every few minutes after submitting. Reports usually arrive within the hour; polling faster just re-checks a job that is still processing.',
127
+ '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.',
128
+ 'A 404 means the job_id does not exist — double-check what scan_contract actually returned rather than retrying blindly.',
129
+ ].join(' '),
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ job_id: { type: 'string', description: 'The job_id returned by scan_contract' },
134
+ },
135
+ required: ['job_id'],
136
+ },
137
+ },
138
+ {
139
+ name: 'list_vulnerability_patterns',
140
+ description: [
141
+ 'Returns historical vulnerability patterns and acceptance rates from Sherlock audit-competition findings.',
142
+ 'Numbers are limited to contests where results could be exactly reconciled against Sherlock\'s own published outcomes (7,388 findings across 35 contests), no estimated or unverifiable figures.',
143
+ 'Useful for understanding what types of bugs are most likely to be valid in a given protocol type.',
144
+ 'This tool is FREE — no payment required.',
145
+ ].join(' '),
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ protocol_type: {
150
+ type: 'string',
151
+ enum: ['DEX', 'LENDING', 'BRIDGE', 'GOVERNANCE', 'STAKING', 'DERIVATIVES', 'GENERAL'],
152
+ description: 'Protocol type to filter patterns (default GENERAL)',
153
+ },
154
+ },
155
+ },
156
+ },
157
+ ],
158
+ }))
159
+
160
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
161
+ const { name, arguments: args } = req.params
162
+
163
+ // ── scan_contract ──────────────────────────────────────────────────────────
164
+ if (name === 'scan_contract') {
165
+ const { repo_url, protocol_type = 'GENERAL' } = args ?? {}
166
+ if (!repo_url) {
167
+ return { content: [{ type: 'text', text: 'Error: repo_url is required' }], isError: true }
168
+ }
169
+
170
+ const { status, json } = await callApi('/api/bug-intel', 'POST', { repo: repo_url, protocolType: protocol_type })
171
+
172
+ if (status === 402) {
173
+ const accept = json.accepts?.[0] ?? {}
174
+ const amtUsdc = accept.amount ? (parseInt(accept.amount) / 1_000_000).toFixed(2) : '5.00'
175
+ if (!accept.payTo || !accept.asset) {
176
+ // Never fall back to a hardcoded address here: a stale one silently
177
+ // sends real USDC to a wallet nobody is watching. Fail loud instead.
178
+ return {
179
+ content: [{
180
+ type: 'text',
181
+ 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.',
182
+ }],
183
+ isError: true,
184
+ }
185
+ }
186
+ return {
187
+ content: [{
188
+ type: 'text',
189
+ text: [
190
+ 'PAYMENT REQUIRED to start the scan.',
191
+ '',
192
+ `Amount: ${amtUsdc} USDC`,
193
+ `Network: Base mainnet (eip155:8453)`,
194
+ `Pay to: ${accept.payTo}`,
195
+ `Asset: USDC — ${accept.asset}`,
196
+ '',
197
+ 'After paying, retry scan_contract with the same repo_url.',
198
+ 'Terms: https://x402.tokenizen.nl/terms',
199
+ ].join('\n'),
200
+ }],
201
+ isError: false,
202
+ }
203
+ }
204
+
205
+ if (status === 200 || status === 201) {
206
+ return {
207
+ content: [{
208
+ type: 'text',
209
+ text: [
210
+ `Scan submitted. Job ID: ${json.jobId}`,
211
+ `Status: ${json.status ?? 'queued'}`,
212
+ '',
213
+ `Poll for results: get_scan_report({ job_id: "${json.jobId}" })`,
214
+ `Reports usually arrive within the hour. The report states how much of the code was read.`,
215
+ ].join('\n'),
216
+ }],
217
+ }
218
+ }
219
+
220
+ return {
221
+ content: [{ type: 'text', text: `API error ${status}: ${JSON.stringify(json).slice(0, 300)}` }],
222
+ isError: true,
223
+ }
224
+ }
225
+
226
+ // ── get_scan_report ────────────────────────────────────────────────────────
227
+ if (name === 'get_scan_report') {
228
+ const { job_id } = args ?? {}
229
+ if (!job_id) {
230
+ return { content: [{ type: 'text', text: 'Error: job_id is required' }], isError: true }
231
+ }
232
+
233
+ const { status, ok, json } = await callApi(`/api/bug-intel/${encodeURIComponent(job_id)}`)
234
+
235
+ if (status === 404) {
236
+ return { content: [{ type: 'text', text: `Job ${job_id} not found. Check the job_id.` }], isError: true }
237
+ }
238
+
239
+ if (!ok || !json.status) {
240
+ return {
241
+ content: [{ type: 'text', text: `Could not get status for job ${job_id}: ${json.error ?? `API error ${status}`}. Try again shortly.` }],
242
+ isError: true,
243
+ }
244
+ }
245
+
246
+ const lines = [`Job ID: ${job_id}`, `Status: ${json.status}`]
247
+ if (json.repo) lines.push(`Repo: ${json.repo}`)
248
+ if (json.reportUrl) lines.push(`Report: ${json.reportUrl}`)
249
+ if (json.findingsCount !== undefined) lines.push(`Findings: ${json.findingsCount}`)
250
+ if (json.status === 'queued' || json.status === 'processing') {
251
+ lines.push('', 'Analysis in progress. Try again in a few minutes.')
252
+ }
253
+
254
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
255
+ }
256
+
257
+ // ── list_vulnerability_patterns ───────────────────────────────────────────
258
+ if (name === 'list_vulnerability_patterns') {
259
+ const { protocol_type = 'GENERAL' } = args ?? {}
260
+ const patterns = ilmPatterns(protocol_type)
261
+
262
+ if (!patterns || patterns.length === 0) {
263
+ return {
264
+ content: [{ type: 'text', text: 'No patterns found for this protocol type. Try GENERAL.' }],
265
+ }
266
+ }
267
+
268
+ const lines = [`Vulnerability patterns for ${protocol_type} protocols (from Sherlock audit findings, exact-reconciled contests only):`, '']
269
+ for (const p of patterns) {
270
+ const pct = Math.round(parseFloat(p.acceptance_rate) * 100)
271
+ lines.push(`${p.pattern.padEnd(24)} ${pct}% accepted (${p.total_cases} cases)`)
272
+ if (p.example) lines.push(` Example: ${p.example}`)
273
+ }
274
+ lines.push('', 'Run scan_contract to check your contracts against these patterns.')
275
+
276
+ return { content: [{ type: 'text', text: lines.join('\n') }] }
277
+ }
278
+
279
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
280
+ })
281
+
282
+ // ── start ─────────────────────────────────────────────────────────────────────
283
+
284
+ const transport = new StdioServerTransport()
285
+ await server.connect(transport)
286
+ // server runs until process exits