@bbradar/mcp 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.3.2 — new Activity data, contest awards and dependency security (2026-11-07)
4
+
5
+ - `find_programs` and `find_hunt_targets` now report the sort direction the API actually applied in `ranking_scope.sort_direction` and `scan.sort_direction`. With no `sort_direction`, the API ranks `first_response` and `time_to_bounty` fastest first (ascending), and these fields previously said `desc`. When the API does not echo a direction, the same rule applies.
6
+ - Fixed Activity, payout, History and analytics results failing in MCP clients that validate structured output against the discovered schema. Those schemas rejected unknown top-level fields, so Status rankings fail validation against an API with `facets` (added 2026-10-13), as do Value pages with `labels` and program payouts with `value_period`. The 21 tools that return one Pro API payload as it is now list these fields and also accept fields the API adds later.
7
+ - `get_activity_rankings` and `get_activity_value` accept `period` (`7d`, `30d` or `90d`) for Value, as the API now does; each period is used only when capabilities advertise it. `24h`, `all_time` and launch cohorts remain local errors. Descriptions cover per-period Value measures and labels, the default landing shortlist named by the page's `labels`, and the 90-day-only signals.
8
+ - `get_program` and `get_program_brief` keep `contest_awards`, the final award allocations of a contest or Huntr challenge, with decimal totals in the contest's own token and a null currency where the platform names none. Pending results stay `null`; other program kinds omit the field. The guide routes finished-contest award questions to `get_program`; a finished contest is readable only there, by exact `platform:handle`, since target reads exclude it. This needs the bbradar-web change that serves awards and finished contests on the API key detail route (`/v1/pro/programs/{program_id}`); before it, the field is absent and finished contests are not found.
9
+ - Updated `@modelcontextprotocol/sdk` to 1.32.2 and `zod` to 4.6.7, fixing the high-severity advisories that failed `npm audit` and the package check (GHSA-22jm-h49p-29qw, GHSA-46jr-4wx8-v453, GHSA-rvq5-wwqv-78pq and GHSA-6prh-2h8m-c8cw in the SDK; GHSA-mrhx-c7xc-m3w5 in zod, which has no 3.x fix), and refreshed patched transitive dependencies (body-parser, fast-uri, hono, `@hono/node-server`, ip-address, qs). Advertised tool fields, defaults and required inputs are unchanged. Zod 4 adds `propertyNames` to record schemas, safe-integer bounds to integers and an RFC 3339 pattern to date-times, and `find_programs.sort_by` stays one flat enum. `updated_since` now rejects date-times without seconds or with an offset lacking its colon, as its `date-time` format always stated.
10
+ - Tool descriptions and the README cover the API's new data: Status `facets`, payout `average_paid` and its sort, Rewards USD display bounds, per-provider Response definitions and windows, program payout facts (`paid_average`, `paid_maximum`, `last_paid_on`, per-severity figures, triage and resolution times, response rate, researcher counts) and `value_period`.
11
+
12
+ ## 0.3.1 — hunt deadlines and target accuracy (2026-10-13)
13
+
14
+ - Reduced unnecessary upstream target rows for small direct reads while preserving offset/limit semantics and full-page scans for local filters. Added a live STDIO check with fresh prefix comparison and cache diagnostics. Catalog concurrency remains four: smaller-pool trials did not establish a reliable improvement under changing API latency.
15
+ - Fixed target inventory pagination in latest-addition and named-program workflows, added language-filter continuations, and exposed remaining offsets. Named target actions now honor target-type and language filters.
16
+ - Marked bounded scope counts and capped legacy exports as incomplete instead of full inventory totals. Export fallbacks reject upstream filters they cannot preserve. Malformed collection pages now fail explicitly rather than becoming empty successes.
17
+ - Rejected unknown arguments across all tools, respected negotiated aggregate page limits, and corrected SDK version diagnostics with a packaged-server regression check. Added live workflow smoke checks that report aggregate measurements and stop on failure without retries.
18
+ - Reserved catalog, recent-change and target-sampling time within the hunt's existing tool deadline. Slow optional reads no longer consume the entire call; completed catalog pages survive later-page timeouts with explicit incomplete coverage, errors and phase timings.
19
+ - Applied existing hunt VDP exclusion upstream when negotiated, preserving unknown classifications and the local filters. Added a read-only live hunt benchmark with aggregate timings and quota reporting, plus deadline, cancellation, coverage and legacy-API regression fixtures. Retained the four-request catalog limit after live concurrency comparisons showed no reliable gain from six.
20
+ - Rejected Activity version IDs on catalog/feed snapshot inputs and validated required signal periods and payout metric/period combinations before sending requests. Tool descriptions and guides distinguish the two snapshot contracts and preserve user filters after empty or partial results.
21
+
3
22
  ## 0.3.0 — Activity and payout views (2026-10-12)
4
23
 
5
24
  - Added five Activity tools to the default 30-tool `core` profile: `get_activity_capabilities`, `get_activity_snapshot`, `get_activity_rankings`, `get_program_payouts`, and `get_activity_change_detail`. Rankings cover payouts, Value, scope, rewards, updates, reports, response, status and all changes. The `full` profile also exposes nine per-view aliases.
package/README.md CHANGED
@@ -116,6 +116,7 @@ Self-Hosted programs use IDs such as `Self-Hosted:program-slug`. They are availa
116
116
 
117
117
  `search_programs` accepts a `search` value and sends it to the API for indexed server-side filtering. Name resolution uses the same path automatically, avoiding broad page scans for exact or partial program names.
118
118
  Use `program_kind: "bounty"`, `"contest"`, or `"challenge"` with `search_programs` or `find_programs` for source-classified program filtering. `find_web3_contests` uses this classification instead of inferring contests from a name or platform.
119
+ `get_program` and `get_program_brief` keep `contest_awards` for a contest or Huntr challenge: the final award allocations its platform publishes (`contest_id`, `currency`, `awarded_total`, `pool_amount`, `awarded_count`, `participants`, `source_url`, `checked_at`, `first_final_at`), or `null` before a final result. Totals are decimal strings in the contest's own token, never USD or a payment record; `currency` is `null` when the platform names none (Huntr). A finished contest with a final result stays readable through `get_program` by exact `platform:handle`, without an opportunity tag; discovery, name resolution and target reads (so also `get_program_brief`) exclude ended contests. The field needs an API whose key-authenticated detail route (`/v1/pro/programs/{program_id}`) serves it; an older API omits it and answers 404 for finished contests.
119
120
  The program catalog now includes confirmed paused platform programs. Raw search preserves `program_status` and `is_active`; ranked discovery excludes paused programs so retained scope and historical rewards cannot be presented as huntable. Exact paused-program reads remain available when explicitly requested.
120
121
 
121
122
  The full tool profile also exposes `search_targets`, a one-request low-level wrapper for reverse search and paginated current target inventory. It supports optional `q`, all API match modes, platform/tag filters, eligibility controls, pages up to the negotiated 500-row limit, and compact or full target output.
@@ -135,6 +136,8 @@ Repository results preserve the API's comparison completeness/method/ancestry ev
135
136
 
136
137
  `search_programs`, `search_targets`, `get_recent_changes`, and `get_repository_changes` accept the API's optional stable `snapshot` token. Send `snapshot: "start"` on page 1 and reuse the returned `meta.snapshot` with identical filters until `meta.snapshot_expires_at`. Snapshot responses bypass the local TTL cache, and an expired snapshot 410 is not mistaken for an expired change-feed cursor. Snapshot creation receives a 60-second minimum request timeout because it performs consistency work and can be materially slower; the fast `find_hunt_targets` workflow intentionally keeps live indexed paging and the ordinary configured timeout.
137
138
 
139
+ Catalog and feed snapshot tokens have at most 32 characters. `find_programs` uses that same contract. The 64-character version ID from `get_activity_snapshot` binds only Activity capabilities, rankings, event details and program payouts; do not pass it to catalog discovery.
140
+
138
141
  ### Activity and payouts
139
142
 
140
143
  Use `get_activity_capabilities` before choosing a ranking. Its versioned views give
@@ -153,7 +156,8 @@ silently replace its period or basis.
153
156
  | `get_activity_change_detail` | One bounded page of an exact Activity event's before/after items |
154
157
 
155
158
  `get_activity_rankings` takes `tab`: `payouts` (reported or tracked, the two bases
156
- kept separate), `value` (frozen Value map and label-specific shortlists),
159
+ kept separate), `value` (frozen Value map and label-specific shortlists for a 7, 30
160
+ or 90-day period),
157
161
  `scope`, `rewards`, `updates`, `reports`, `response`, `status` (lifecycle events;
158
162
  first seen is not a launch date) or `all_changes` (catalog events, including
159
163
  eligible events for departed programs). Each tab accepts only its own inputs:
@@ -174,9 +178,30 @@ An API that does not advertise the filter is told so before any request.
174
178
 
175
179
  Ranking pages omit the capabilities document the API repeats on every page; read it
176
180
  once with `get_activity_capabilities`. When a tab has no enabled default view the
177
- empty page carries that tab's `available_views` instead.
178
-
179
- `tab: "value"` has no period input. With `include_map: true` its `points` cover the
181
+ empty page carries that tab's `available_views` instead. Status pages add `facets`:
182
+ event counts per `change_type` and `current_status` for the period and filters, each
183
+ counted under every other filter but not its own selection, with zero counts omitted.
184
+
185
+ Payout rows also carry `average_paid`: the platform's own current average payment
186
+ (an exact mean, or HackerOne's 45th to 55th percentile range) with its
187
+ `source_label`, `period`, native currency and USD conversion. It is the same in every
188
+ period, and `sort: "average_paid"` ranks it where capabilities advertise it. Rewards
189
+ rows add `minimum_usd`/`maximum_usd` to `before`/`after` at the version's current
190
+ quotes for display; the change percentage still compares native maxima. Response
191
+ cells keep each provider's definition, label and window (`first_response_label`,
192
+ `first_response_window`, `first_response_precision`, `time_to_bounty_window`), and
193
+ duration sorts (`first_response`, `time_to_bounty`) rank fastest first when
194
+ `direction` is omitted.
195
+
196
+ `tab: "value"` takes `period` `7d`, `30d` or `90d`, each only once
197
+ `get_activity_capabilities` advertises it: report volume covers that period, while
198
+ payout per processed report is lifetime. Rows and points carry the period's count and
199
+ percentile in `value`; `signals` keep the lifetime and 90-day figures. Without a
200
+ period, a request naming `labels` or `sort` means 90 days, and a bare request opens
201
+ the advertised default view, which can be a label shortlist: the page's `labels`
202
+ names the list applied (empty for the map's programs). High value, Popular and Low
203
+ per report exist in each period; Pays near top, No payouts seen and processed versus
204
+ received exist only at 90 days. With `include_map: true` its `points` cover the
180
205
  full filtered map population from the same snapshot (up to 5,000); by default the
181
206
  map is replaced by `map_points_omitted` with its count, because thousands of points
182
207
  rarely help a text answer. `rows` are a separately paginated shortlist. Percentiles stay frozen within their original platform cohorts after
@@ -187,8 +212,14 @@ rejected rather than replaced.
187
212
 
188
213
  Rankings and `get_program_payouts` return server-owned `signals`, `labels` and
189
214
  `label_details`, including explanations and destinations. Program detail also
190
- returns `value_points` for same-platform peers when called with `include_map: true`
191
- (otherwise `map_points_omitted` gives their count). “Last payout seen” describes an
215
+ returns `value_points` for same-platform peers in the Value period named by
216
+ `value_period` when called with `include_map: true` (otherwise `map_points_omitted`
217
+ gives their count). Its `facts` also carry each platform's own published figures,
218
+ each with its `measure`, `unit`, `period` and `source_label`: `paid_average`,
219
+ `paid_maximum` and `last_paid_on`, HackerOne's 90-day `paid_average_<severity>` and
220
+ lifetime `severity_reports_<severity>`, triage and resolution times, PatchDay's
221
+ response rate and researcher counts. They are program-detail facts with no Activity
222
+ column or sort; keep each definition rather than comparing platforms as one measure. “Last payout seen” describes an
192
223
  observed counter increase, never a payment date. “Pays near top” is the platform's
193
224
  top-quarter rule: retain the actual amount/percentage in the explanation. Processed
194
225
  vs received can exceed 100% and does not measure response speed. Never recalculate
@@ -236,8 +267,10 @@ Reports and resolved counters support `7d`, `30d`, `90d`, `all_time`, and API-on
236
267
  `24h` where enabled. All-time uses the fresh lifetime counter; it has no Previous
237
268
  or Change. Finite report comparisons retain the same basis within each program;
238
269
  report volume does not establish researcher competition. Scope, Rewards, Updates,
239
- Status and All changes support only `7d`, `30d` and `90d`. Response counts retain
240
- the provider's term; time to bounty measures award timing, not settlement. Label
270
+ Status and All changes support only `7d`, `30d` and `90d`. Response publishes only
271
+ `7d` and `30d` until resolved counters hold 90 days of history; use the periods
272
+ capabilities advertise. Response counts retain the provider's term (resolved,
273
+ accepted, paid or rewarded); time to bounty measures award timing, not settlement. Label
241
274
  filters must come from current capabilities, and unsupported labels remain errors.
242
275
 
243
276
  Use exact `platform:handle` identities for `get_program_payouts`; slash-containing
@@ -246,6 +279,11 @@ definitions and source timestamps remain intact in structured output. Human
246
279
  summaries should show the available numbers and units, keeping internal collection
247
280
  and completeness metadata in the structured result.
248
281
 
282
+ Tools that return one Pro API payload as it is (the Activity, payout, History and
283
+ analytics tools) list its known fields in their output schemas and also accept new
284
+ top-level fields. Clients validate structured results against the schema they
285
+ discovered, so a field the API adds later reaches them instead of failing the call.
286
+
249
287
  `find_programs` and `find_hunt_targets` accept combined Activity predicates
250
288
  (`search_programs` and `get_opportunities` reject them; use `find_programs`, with one
251
289
  `opportunity_levels` entry for a tier). The API applies them together before
@@ -253,7 +291,9 @@ pagination, preserving one global ordering and one stable snapshot. For example,
253
291
  `{"payout_period":"30d","payout_metric":"payout_increase","payout_min":1000,"reports_period":"7d","reports_max":10,"sort_by":"payout_amount","sort_direction":"desc"}`
254
292
  to combine measured payouts in the last 30 days with report activity in the last
255
293
  7 days. Add `snapshot: "start"` on the first page and reuse `meta.snapshot` with
256
- identical filters. The default order remains `updated` descending. Bounds sorts
294
+ identical filters. The default order remains `updated` descending; an omitted
295
+ `sort_direction` for `first_response` or `time_to_bounty` means ascending (fastest
296
+ first), and `ranking_scope.sort_direction` reports the order applied. Bounds sorts
257
297
  require `proven_bounds`: `payout_lower_bound` descends and `payout_upper_bound`
258
298
  ascends. `payout_positive: false` requires an exact observed zero; omit it when
259
299
  both zero and positive payers should qualify.
@@ -344,6 +384,22 @@ Already-retired historical events cannot be reconstructed. `get_program_targets`
344
384
  follows upstream API pages until it can satisfy the requested offset/limit or the
345
385
  inventory ends. Missing public scope is treated as out of scope.
346
386
 
387
+ Target lists expose `meta.has_more` and `meta.next_offset`; repeat the same filters
388
+ with that offset to continue. `language_tags` applies while scanning pages, so
389
+ sparse matches do not skip eligible targets. Named target actions honor their
390
+ target-type and language filters. `get_latest_added_targets` also pages its optional
391
+ inventory and returns `full_targets_next_offset` for `get_program_targets`.
392
+ Scope summaries and breakdowns label sampled counts with `count_scope` and
393
+ `possibly_incomplete`; unknown full filtered totals are omitted. Legacy export
394
+ fallbacks disclose their cap and reject filters whose upstream semantics they
395
+ cannot preserve. All tools reject unknown arguments, and malformed collection
396
+ responses return errors instead of claiming the inventory is empty.
397
+
398
+ Small direct target reads request only the prefix needed for their offset and limit,
399
+ up to the negotiated page size, instead of always loading a 500-row page. Local
400
+ scope and language post-filters retain full pages to find sparse matches. This
401
+ reduces excess transfer and processing while preserving fresh reads and row offsets.
402
+
347
403
  Cursors are memory-only by default. Set `BBRADAR_MCP_CURSOR_STATE_PATH` to opt into restart-safe cursor persistence. The state file contains only endpoint-prefixed SHA-256 filter hashes and non-negative numeric IDs—never raw filters, API responses, program or target records, credentials, or authorization headers.
348
404
 
349
405
  For cacheable responses, large target and change collections return at most 50 inline records by default; the remainder is held in a bounded, ten-minute continuation resource. Private or `no-store` pages stay in the current structured result and are never put in a reusable continuation. Compact tool text keeps model context small.
@@ -379,6 +435,24 @@ Call `get_mcp_status` to inspect cache and stale-hit rates, circuit states, curs
379
435
 
380
436
  Run `npm run benchmark` for deterministic local benchmarks. They report request and byte counts, model-context bytes, latency percentiles, and memory under representative History and analytics concurrency. Fixture timings are separate from live API latency.
381
437
 
438
+ With `BBRADAR_API_KEY` already configured, run `npm run build` then `node benchmark/live-hunt.mjs` for one authenticated all-platform Web2 hunt (domain/API, $5,000 maximum bounty, at least three eligible and five total targets, 15 results). It reports aggregate phase timings, request counts, quota and sampled coverage without printing the key or program/target records. This consumes live API quota and makes no automatic retries. Live catalog updates can change rows between pages; signatures help distinguish result changes from timing changes.
439
+
440
+ `node benchmark/live-api-check.mjs` checks negotiated capabilities, discovery,
441
+ all nine Activity tabs, program and target reads, payouts, History, daily analytics,
442
+ feeds, combined filters and the same hunt. It emits aggregate measurements and
443
+ stops at the first failure. An optional section (`activity`, `programs`, `analytics`,
444
+ `feeds`, or `hunt`) runs only that workflow and its prerequisites, so independent
445
+ checks can continue without repeating successful sections. These are live reads
446
+ and consume quota; a smoke pass is not an uptime or complete-catalog guarantee.
447
+
448
+ `node benchmark/live-stdio.mjs` performs the original hunt through the actual
449
+ STDIO server entry point using the configured key, then compares a fresh small
450
+ target page with the prefix of a fresh wider page. Use `targets` to run the target
451
+ check independently. It reports cache activity, timings and counts, never target
452
+ identifiers. Partial hunts fail the benchmark instead of counting as speed gains.
453
+ `live-hunt.mjs --catalog-concurrency=2` can test a lower catalog request cap;
454
+ inspect both timings and coverage before adopting a concurrency change.
455
+
382
456
  Maintainers can compare the working tree against a committed revision with `npm run build` followed by `node --expose-gc benchmark/compare-revisions.mjs HEAD`. This uses the same fixtures and alternating samples for both revisions, reports discovery and result sizes, and measures repeated private reads separately from cacheable reads. It requires the repository and development dependencies.
383
457
 
384
458
  Run `npm run test:package` to pack the release, install its production dependencies in a temporary directory, audit them, and exercise the packaged STDIO server. CI runs the checks on Node 20, 22, and 24.
@@ -393,6 +467,12 @@ Use `find_hunt_targets` as the default one-call path when a request combines goo
393
467
 
394
468
  `find_hunt_targets` supports arbitrary platform inclusion/exclusion, scope/target/language filters, reward and breadth thresholds, Web2/Web3 modes, target-intelligence filters, freshness windows, `free_submission_only`, and balanced/reward/scope/freshness/competition ranking. Activity inputs such as `payout_period: "30d"` with `payout_positive: true`, or `scope_period: "30d"` with `scope_added_min: 1`, first narrow its candidates on the server in global order; the remaining filters and `ranking_mode` then apply locally, and `scan` reports how much of the server result was examined. Unknown inputs are rejected rather than ignored. It suppresses bulk `bbradar://` resource links while preserving each program's public BBRadar and platform URLs, preventing large shortlists from flooding model context.
395
469
 
470
+ The hunt reserves time within the configured tool deadline: catalog discovery uses at most 60%, optional recent-change queries 30% in parallel, and target sampling finishes by 90%, leaving time to return the result. With the default 30 seconds, those bounds are 18, 9 and 27 seconds. A stalled later catalog page preserves completed pages; Activity scans retain only their completed ordered prefix. A failed first catalog page remains an error. Partial reads include `errors`, warnings, `meta.partial_success`, `result_guidance.coverage_incomplete` and `meta.phase_timing`. Missing enrichment is unavailable evidence. Keep the user's platform scope and thresholds; a sampled change feed cannot identify the complete supported platform set.
471
+
472
+ Legacy catalog hunts apply their existing VDP exclusion upstream when the API advertises that mode, then retain the local filters. Programs with unknown classifications and positive rewards remain eligible. Older APIs keep local exclusion. Catalog paging retains its four-request limit within the configured global pool; Activity scans keep their separate two-request limit.
473
+
474
+ Combined Activity filters need independent periods. `change_category` requires `updates_period`; filtering additions with `scope_period` and `scope_added_min` alone needs no category. `payout_total` requires `payout_period: "all_time"`; use `payout_increase` for tracked 24h/7d/30d/90d changes, or an enabled `payout_native_90d` comparison with 90d for provider-reported values. Invalid combinations fail locally with guidance. An empty valid result does not authorize changing requested thresholds.
475
+
396
476
  ```json
397
477
  {
398
478
  "platforms": ["Immunefi", "HackenProof", "BugCrowd", "YesWeHack"],
@@ -0,0 +1,126 @@
1
+ // Read-only production smoke check. Requires BBRADAR_API_KEY; emits aggregate
2
+ // measurements only and stops at the first failed call without retrying.
3
+ // Usage: node benchmark/live-api-check.mjs [all|activity|programs|analytics|feeds|hunt]
4
+ import assert from "node:assert/strict";
5
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
7
+ import { BBRadarClient } from "../dist/bbradarClient.js";
8
+ import { loadConfig } from "../dist/config.js";
9
+ import { createBbradarServer } from "../dist/server.js";
10
+
11
+ const config = loadConfig();
12
+ config.toolProfile = "core";
13
+ const section = process.argv[2] ?? "all";
14
+ assert.ok(["all", "activity", "programs", "analytics", "feeds", "hunt"].includes(section), "Use all, activity, programs, analytics, feeds, or hunt.");
15
+ const enabled = name => section === "all" || section === name;
16
+ const upstream = new BBRadarClient(config);
17
+ const observations = [];
18
+ const report = { measured_at: new Date().toISOString(), fixture: false, section, observations };
19
+ let server, client;
20
+ async function call(name, args = {}) {
21
+ const before = upstream.diagnostics().upstream_requests.actual_requests;
22
+ const result = await client.callTool({ name, arguments: args });
23
+ const data = result.structuredContent;
24
+ observations.push({ tool: name, tab: args.tab, success: !result.isError,
25
+ status: data?.error?.status,
26
+ error: data?.error?.message?.replaceAll(config.apiKey, "[redacted]"),
27
+ duration_ms: data?.mcp_timing?.duration_ms,
28
+ requests: upstream.diagnostics().upstream_requests.actual_requests - before,
29
+ rows: (data?.rows ?? data?.programs ?? data?.targets ?? data?.changes ?? data?.series ?? data?.matches)?.length,
30
+ no_store: data?.cache?.no_store, partial_success: data?.meta?.partial_success,
31
+ coverage_incomplete: data?.result_guidance?.coverage_incomplete,
32
+ phase_timing: data?.meta?.phase_timing });
33
+ if (result.isError) throw new Error(`${name} failed with status ${data?.error?.status ?? "unknown"}`);
34
+ return data;
35
+ }
36
+
37
+ try {
38
+ await upstream.negotiateCapabilities();
39
+ const contract = upstream.capabilitySnapshot();
40
+ assert.equal(contract.status, "negotiated");
41
+ report.contract_version = contract.contract.contract_version;
42
+ report.api_endpoints = Object.keys(contract.contract.endpoints).sort();
43
+ server = createBbradarServer(upstream, config);
44
+ client = new Client({ name: "bbradar-live-check", version: "1" });
45
+ const [a, b] = InMemoryTransport.createLinkedPair();
46
+ await Promise.all([client.connect(a), server.connect(b)]);
47
+ const discovery = await client.listTools();
48
+ report.core_tool_count = discovery.tools.length;
49
+ report.discovery_bytes = Buffer.byteLength(JSON.stringify(discovery));
50
+ const status = await call("get_mcp_status");
51
+ report.server_version = status.status.server_version;
52
+ report.sdk_version = status.status.sdk_version;
53
+ await call("get_bbradar_guide");
54
+ let snapshot;
55
+ if (enabled("activity") || enabled("programs")) {
56
+ const version = await call("get_activity_snapshot");
57
+ snapshot = version.snapshot_id;
58
+ }
59
+ if (enabled("activity")) {
60
+ const capabilities = await call("get_activity_capabilities", { snapshot });
61
+ for (const tab of capabilities.tabs) {
62
+ // Let the server select its advertised default; an unqualified default
63
+ // legitimately returns no rows plus available_views, never an invented view.
64
+ const page = await call("get_activity_rankings", { tab: tab.id, snapshot, limit: 1 });
65
+ assert.equal(page.tab, tab.id);
66
+ assert.equal(page.meta.snapshot_id, snapshot);
67
+ assert.equal(page.cache.no_store, true);
68
+ assert.ok(Array.isArray(page.rows));
69
+ if (page.sort === null) assert.ok(Array.isArray(page.available_views));
70
+ }
71
+ }
72
+ let program;
73
+ if (enabled("programs") || enabled("analytics")) {
74
+ const catalog = await call("search_programs", { page_size: 5 });
75
+ program = catalog.programs.find(row => row.platform !== "Self-Hosted");
76
+ assert.ok(program?.id, "The bounded live catalog had no program for the detail checks.");
77
+ }
78
+ if (enabled("programs")) {
79
+ await call("get_program", { program_id: program.id });
80
+ await call("get_program_targets", { program_id: program.id, limit: 2 });
81
+ await call("get_program_payouts", { program_id: program.id, snapshot });
82
+ await call("get_program_activity", { program_id: program.id });
83
+ await call("get_program_history", { program_id: program.id, limit: 1 });
84
+ await call("get_history_changes", { limit: 1 });
85
+ }
86
+ if (enabled("analytics")) {
87
+ await call("get_analytics_platform_series", { platform: program.platform, limit: 1 });
88
+ await call("get_analytics_program_detail", { program_id: program.id, limit: 1 });
89
+ await call("get_analytics_program_activity", { limit: 1 });
90
+ }
91
+ if (enabled("feeds")) {
92
+ await call("get_recent_changes", { page_size: 1 });
93
+ await call("get_repository_changes", { page_size: 1, include_targets: false });
94
+ await call("get_opportunities", { level: "elite", page_size: 1 });
95
+ const combined = await call("find_programs", {
96
+ payout_period: "30d", payout_positive: true, require_bounty: true, max_pages: 1, max_results: 1
97
+ });
98
+ assert.ok(combined.scan);
99
+ }
100
+ if (enabled("hunt")) {
101
+ const hunt = await call("find_hunt_targets", {
102
+ surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
103
+ min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
104
+ include_recent_changes: true, include_unknown_report_count: true,
105
+ recent_target_days: 90, max_results: 15, targets_per_result: 3,
106
+ ranking_mode: "balanced", upstream_request_budget: 50
107
+ });
108
+ assert.ok(hunt.matches.length > 0);
109
+ assert.equal(hunt.filters.min_bounty_max, 5000);
110
+ assert.equal(hunt.filters.surface_mode, "web2");
111
+ assert.ok(hunt.upstream_budget.used <= 50);
112
+ report.hunt = { returned: hunt.matches.length, target_samples: hunt.matches.reduce((sum, row) => sum + row.target_samples.length, 0),
113
+ ranking_scope: hunt.ranking_scope, upstream_budget: hunt.upstream_budget, partial_success: hunt.meta.partial_success };
114
+ if (hunt.meta.partial_success) process.exitCode = 1;
115
+ }
116
+ } catch (error) {
117
+ report.failure = String(error.message).replaceAll(config.apiKey, "[redacted]");
118
+ process.exitCode = 1;
119
+ } finally {
120
+ await client?.close();
121
+ await server?.close();
122
+ report.requests = upstream.diagnostics().upstream_requests.actual_requests;
123
+ report.concurrency = upstream.diagnostics().request_concurrency;
124
+ report.request_window = upstream.diagnostics().upstream_request_window;
125
+ console.log(JSON.stringify(report, null, 2));
126
+ }
@@ -0,0 +1,105 @@
1
+ // One authenticated read-only hunt. Only aggregate measurements reach stdout;
2
+ // credentials and program/target records are never written to disk.
3
+ // With BBRADAR_API_KEY configured: node benchmark/live-hunt.mjs
4
+ // Optional experiment: node benchmark/live-hunt.mjs --catalog-concurrency=2
5
+ // This only lowers catalog concurrency; it never changes filters or page counts.
6
+ import assert from "node:assert/strict";
7
+ import { createHash } from "node:crypto";
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
+ import { BBRadarClient } from "../dist/bbradarClient.js";
11
+ import { loadConfig } from "../dist/config.js";
12
+ import { createBbradarServer } from "../dist/server.js";
13
+
14
+ const args = {
15
+ surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
16
+ min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
17
+ include_recent_changes: true, include_unknown_report_count: true,
18
+ recent_target_days: 90, max_results: 15, targets_per_result: 3,
19
+ ranking_mode: "balanced", upstream_request_budget: 50
20
+ };
21
+ const config = loadConfig();
22
+ const catalogOption = process.argv.slice(2);
23
+ assert.ok(catalogOption.length <= 1 && (!catalogOption.length || /^--catalog-concurrency=[1-4]$/.test(catalogOption[0])),
24
+ "Use --catalog-concurrency=1, 2, 3 or 4, or omit it for the server default.");
25
+ const catalogConcurrency = catalogOption.length ? Number(catalogOption[0].split("=")[1]) : Number.POSITIVE_INFINITY;
26
+ let activeCatalog = 0;
27
+ let peakCatalog = 0;
28
+ const catalogWaiters = [];
29
+ async function acquireCatalog() {
30
+ if (activeCatalog < catalogConcurrency) {
31
+ activeCatalog += 1;
32
+ peakCatalog = Math.max(peakCatalog, activeCatalog);
33
+ return;
34
+ }
35
+ await new Promise(resolve => catalogWaiters.push(resolve));
36
+ }
37
+ function releaseCatalog() {
38
+ const next = catalogWaiters.shift();
39
+ if (next) next(); else activeCatalog -= 1;
40
+ }
41
+ const upstream = new BBRadarClient(config);
42
+ const request = upstream.request.bind(upstream);
43
+ const redact = value => String(value).replaceAll(config.apiKey, "[redacted]");
44
+ let timings = [];
45
+ let catalogIds = new Set();
46
+ upstream.request = async options => {
47
+ const start = performance.now();
48
+ const endpoint = options.path.replace(/\/programs\/.+\/targets$/, "/programs/:program_id/targets");
49
+ const isCatalog = options.path === "/pro/programs";
50
+ if (isCatalog) await acquireCatalog();
51
+ try {
52
+ const result = await request(options);
53
+ for (const program of result.data?.programs ?? []) catalogIds.add(program.id);
54
+ timings.push({ endpoint, page: options.query?.page, elapsed_ms: Math.round(performance.now() - start),
55
+ status: result.status, cached: result.cached, coalesced: result.coalesced,
56
+ rows: result.data?.programs?.length ?? result.data?.results?.length ?? result.data?.targets?.length });
57
+ return result;
58
+ } catch (error) {
59
+ timings.push({ endpoint, page: options.query?.page, elapsed_ms: Math.round(performance.now() - start),
60
+ status: error.status ?? null, error: redact(error.message) });
61
+ throw error;
62
+ } finally {
63
+ if (isCatalog) releaseCatalog();
64
+ }
65
+ };
66
+ const hash = values => createHash("sha256").update(JSON.stringify([...values].sort())).digest("hex");
67
+
68
+ try {
69
+ await upstream.negotiateCapabilities();
70
+ const server = createBbradarServer(upstream, config);
71
+ const client = new Client({ name: "bbradar-live-benchmark", version: "1" });
72
+ const [a, b] = InMemoryTransport.createLinkedPair();
73
+ await Promise.all([client.connect(a), server.connect(b)]);
74
+ try {
75
+ timings = [];
76
+ catalogIds = new Set();
77
+ const before = upstream.diagnostics().upstream_requests.actual_requests;
78
+ const result = await client.callTool({ name: "find_hunt_targets", arguments: args });
79
+ const data = result.structuredContent;
80
+ const matches = data?.matches ?? [];
81
+ const observation = {
82
+ concurrency: config.maxConcurrentRequests,
83
+ catalog_concurrency_cap: Number.isFinite(catalogConcurrency) ? catalogConcurrency : "server_default",
84
+ catalog_request_peak: peakCatalog, is_error: result.isError ?? false,
85
+ error: data?.error ? { status: data.error.status, message: redact(data.error.message) } : undefined,
86
+ timing: data?.mcp_timing, phase_timing: data?.meta?.phase_timing,
87
+ returned: matches.length, target_samples: matches.reduce((count, match) => count + match.target_samples.length, 0),
88
+ programs_without_target_samples: matches.filter(match => !match.target_samples.length).length,
89
+ partial_success: data?.meta?.partial_success, coverage_incomplete: data?.result_guidance?.coverage_incomplete,
90
+ ranking_scope: data?.ranking_scope,
91
+ upstream_budget: data?.upstream_budget, actual_requests: upstream.diagnostics().upstream_requests.actual_requests - before,
92
+ catalog_rows: catalogIds.size, catalog_signature: hash(catalogIds),
93
+ shortlist_signature: hash(matches.map(match => match.program.id)),
94
+ errors: data?.errors?.map(error => ({ status: error.status, message: redact(error.message) })),
95
+ timings
96
+ };
97
+ if (result.isError || data?.meta?.partial_success) process.exitCode = 1;
98
+ console.log(JSON.stringify({ fixture: false, measured_at: new Date().toISOString(), arguments: args, observations: [observation],
99
+ quota: upstream.diagnostics().upstream_rate_limit,
100
+ request_window: upstream.diagnostics().upstream_request_window }, null, 2));
101
+ } finally { await client.close(); await server.close(); }
102
+ } catch (error) {
103
+ console.error(JSON.stringify({ status: error.status ?? null, message: redact(error.message) }));
104
+ process.exitCode = 1;
105
+ }
@@ -0,0 +1,80 @@
1
+ // Authenticated production smoke check through the actual STDIO entry point.
2
+ // Emits aggregates only; never writes credentials or program/target records.
3
+ // node benchmark/live-stdio.mjs [hunt|targets]
4
+ import assert from "node:assert/strict";
5
+ import { createHash } from "node:crypto";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9
+ import { loadConfig } from "../dist/config.js";
10
+
11
+ const config = loadConfig();
12
+ const mode = process.argv[2] ?? "hunt";
13
+ assert.ok(["hunt", "targets"].includes(mode), "Use hunt or targets.");
14
+ const client = new Client({ name: "bbradar-live-stdio", version: "1" });
15
+ const transport = new StdioClientTransport({ command: process.execPath,
16
+ args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))], stderr: "pipe",
17
+ env: { BBRADAR_API_KEY: config.apiKey, BBRADAR_API_BASE_URL: config.apiBaseUrl,
18
+ BBRADAR_MCP_TOOL_PROFILE: "core", BBRADAR_MCP_VALIDATE_ON_STARTUP: "true" } });
19
+ const report = { measured_at: new Date().toISOString(), fixture: false, transport: "stdio", mode };
20
+ async function call(name, args = {}) {
21
+ const result = await client.callTool({ name, arguments: args });
22
+ if (result.isError) throw new Error(`${name}: ${result.structuredContent?.error?.message ?? "MCP call failed"}`);
23
+ return result.structuredContent;
24
+ }
25
+ try {
26
+ await client.connect(transport);
27
+ const tools = await client.listTools();
28
+ report.core_tools = tools.tools.length;
29
+ const before = (await call("get_mcp_status")).status.client;
30
+ assert.equal(before.capability_negotiation.status, "negotiated");
31
+ let programs;
32
+ if (mode === "hunt") {
33
+ const hunt = await call("find_hunt_targets", {
34
+ surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
35
+ min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
36
+ include_recent_changes: true, include_unknown_report_count: true,
37
+ recent_target_days: 90, max_results: 15, targets_per_result: 3,
38
+ ranking_mode: "balanced", upstream_request_budget: 50
39
+ });
40
+ report.hunt = { duration_ms: hunt.mcp_timing.duration_ms, phase_timing: hunt.meta.phase_timing,
41
+ programs: hunt.matches.length, target_samples: hunt.matches.reduce((n, row) => n + row.target_samples.length, 0),
42
+ upstream_requests: hunt.upstream_budget.used, partial_success: hunt.meta.partial_success,
43
+ ranking_scope: hunt.ranking_scope, catalog_pages_read: hunt.source_requests.filter(row => row.source === "programs").length,
44
+ errors: hunt.errors?.map(error => ({ status: error.status, message: error.message?.replaceAll(config.apiKey, "[redacted]") })),
45
+ shortlist_signature: createHash("sha256").update(JSON.stringify(hunt.matches.map(row => row.program.id).sort())).digest("hex") };
46
+ assert.equal(hunt.meta.partial_success, false, "The hunt returned incomplete work; do not count it as a successful performance run.");
47
+ assert.ok(hunt.matches.length > 0);
48
+ programs = hunt.matches.map(row => row.program);
49
+ } else {
50
+ programs = (await call("search_programs", { page_size: 10 })).programs;
51
+ }
52
+ // Compare the requested prefix with a fresh wider read. A live source change
53
+ // remains a failed comparison, never an excuse to substitute old data.
54
+ const selected = [...programs].sort((a, b) => (b.scope_summary?.target_counts?.eligible_for_bounty ?? 0)
55
+ - (a.scope_summary?.target_counts?.eligible_for_bounty ?? 0))[0];
56
+ assert.ok(selected?.id, "The bounded catalog returned no target candidate.");
57
+ const args = { program_id: selected.id, output_mode: "identifiers" };
58
+ const wide = await call("get_program_targets", { ...args, limit: 500 });
59
+ const small = await call("get_program_targets", { ...args, limit: 3 });
60
+ assert.ok(wide.targets.length > 0);
61
+ assert.ok(JSON.stringify(wide.targets.slice(0, 3)) === JSON.stringify(small.targets), "Fresh target prefixes differ; inspect source drift before claiming equivalence.");
62
+ assert.equal(wide.cache.no_store, true);
63
+ assert.equal(small.cache.no_store, true);
64
+ report.target_prefix = { equal: true, wide_read_rows: wide.targets.length, small_read_rows: small.targets.length,
65
+ wide_read_ms: wide.mcp_timing.duration_ms, small_read_ms: small.mcp_timing.duration_ms,
66
+ small_read_has_more: small.meta.has_more, small_read_next_offset: small.meta.next_offset, both_no_store: true };
67
+ const after = (await call("get_mcp_status")).status.client;
68
+ report.upstream_requests = after.upstream_requests.actual_requests - before.upstream_requests.actual_requests;
69
+ report.response_cache = after.response_cache;
70
+ report.request_window = after.upstream_request_window;
71
+ assert.equal(after.request_concurrency.active, 0);
72
+ assert.equal(after.request_concurrency.queued, 0);
73
+ } catch (error) {
74
+ report.failure = String(error.message).replaceAll(config.apiKey, "[redacted]");
75
+ process.exitCode = 1;
76
+ } finally {
77
+ await client.close();
78
+ await transport.close();
79
+ console.log(JSON.stringify(report, null, 2));
80
+ }
@@ -1,2 +1,4 @@
1
1
  /** Catalog metadata stays available when Activity computation is disabled. */
2
2
  export declare function requiresActivityQuery(input?: Record<string, unknown>): boolean;
3
+ /** Validate dependencies without supplying or changing the user's periods. */
4
+ export declare function validateActivityCatalogInput(input: Record<string, unknown>): void;
@@ -12,4 +12,36 @@ export function requiresActivityQuery(input = {}) {
12
12
  return Object.entries(input).some(([key, value]) => value !== undefined && value !== null
13
13
  && (key === "sort_by" ? value !== "updated" : ACTIVITY_FILTERS.has(key)));
14
14
  }
15
+ /** Validate dependencies without supplying or changing the user's periods. */
16
+ export function validateActivityCatalogInput(input) {
17
+ const requirements = {
18
+ scope: ["scope_added_min", "scope_removed_min", "scope_net_min", "scope_net_max"],
19
+ reward: ["reward_change", "reward_change_min_pct"],
20
+ updates: ["active_days_min", "changes_min", "change_category"],
21
+ reports: ["reports_min", "reports_max", "reports_change", "reports_change_min_pct"],
22
+ resolved: ["resolved_min"], status: ["status_change"],
23
+ payout: ["payout_min", "payout_max", "payout_metric", "payout_activity", "payout_positive", "payout_precision"]
24
+ };
25
+ const sortPeriods = {
26
+ payout_amount: "payout", payout_lower_bound: "payout", payout_upper_bound: "payout",
27
+ scope_added: "scope", scope_net: "scope", reward_change: "reward", active_days: "updates",
28
+ last_change: "updates", reports: "reports", reports_change: "reports", resolved: "resolved"
29
+ };
30
+ for (const [prefix, keys] of Object.entries(requirements)) {
31
+ const signal = keys.find((key) => input[key] !== undefined);
32
+ if ((signal || sortPeriods[String(input.sort_by)] === prefix) && input[`${prefix}_period`] === undefined) {
33
+ throw new Error(`Specify ${prefix}_period for ${signal ?? `sort_by=${String(input.sort_by)}`}. Periods are never inferred from another signal.`);
34
+ }
35
+ }
36
+ const period = input.payout_period;
37
+ const metric = input.payout_metric ?? (period === "all_time" ? "payout_total" : "payout_increase");
38
+ if (period !== undefined && ((metric === "payout_total") !== (period === "all_time")
39
+ || (metric === "payout_native_90d" && period !== "90d"))) {
40
+ throw new Error("Payout metric and period are incompatible: payout_total requires all_time; payout_increase uses 24h, 7d, 30d or 90d; payout_native_90d requires 90d. Keep the requested period and choose its supported metric.");
41
+ }
42
+ if (input.payout_activity !== undefined && (metric !== "payout_increase"
43
+ || (input.payout_activity !== "increase" && period !== "30d"))) {
44
+ throw new Error("payout_activity requires payout_increase; no_increase and lower require payout_period=30d.");
45
+ }
46
+ }
15
47
  //# sourceMappingURL=activityContract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"activityContract.js","sourceRoot":"","sources":["../src/activityContract.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY;IACnG,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,mBAAmB;IAC5F,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB;IAC3F,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB;IACvF,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,iBAAiB;IAC3F,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,0BAA0B;IACrF,0BAA0B,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB;CAClG,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,QAAiC,EAAE;IACvE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;WACpF,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
1
+ {"version":3,"file":"activityContract.js","sourceRoot":"","sources":["../src/activityContract.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ export type ApiRequestOptions = {
10
10
  staleWhileRevalidateMs?: number;
11
11
  timeoutMs?: number;
12
12
  signal?: AbortSignal;
13
+ expectedArray?: "programs" | "targets" | "results";
13
14
  };
14
15
  export type ApiReadControls = Pick<ApiRequestOptions, "cache" | "coalesce" | "timeoutMs" | "signal">;
15
16
  export type ApiResult<T> = {
@@ -73,7 +74,10 @@ export declare class BBRadarClient {
73
74
  private capabilityNegotiation;
74
75
  private capabilityState;
75
76
  constructor(config: ServerConfig);
76
- withRequestContext<T>(signal: AbortSignal, callback: () => T | Promise<T>, snapshotStart?: boolean): Promise<T>;
77
+ withRequestContext<T>(signal: AbortSignal, callback: () => T | Promise<T>, snapshotStart?: boolean, timeoutMs?: number): Promise<T>;
78
+ /** Give one aggregate stage a shorter budget without extending its parent. */
79
+ withRequestTimeout<T>(timeoutMs: number, callback: () => T | Promise<T>): Promise<T>;
80
+ throwIfRequestAborted(): void;
77
81
  diagnostics(): {
78
82
  response_cache: {
79
83
  entries: number;