@bbradar/mcp 0.2.6 → 0.3.1
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 +25 -0
- package/README.md +230 -3
- package/benchmark/live-api-check.mjs +126 -0
- package/benchmark/live-hunt.mjs +105 -0
- package/benchmark/live-stdio.mjs +80 -0
- package/benchmark/pro-benchmark.mjs +26 -4
- package/dist/activityContract.d.ts +4 -0
- package/dist/activityContract.js +47 -0
- package/dist/activityContract.js.map +1 -0
- package/dist/bbradarClient.d.ts +10 -1
- package/dist/bbradarClient.js +78 -16
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.js +3 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +814 -137
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — hunt deadlines and target accuracy (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
## 0.3.0 — Activity and payout views (2026-10-12)
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- Each ranking reads one bounded server page. Dynamic capabilities describe eligible period, basis, precision, sort and direction combinations; unsupported explicit views remain errors without period substitution.
|
|
17
|
+
- Rankings, capabilities and program payouts accept a published Activity `snapshot` binding. Snapshot IDs, opaque cursors, frozen cutoffs and 410 recovery guidance keep reads on one version. Activity event detail requires the originating page's snapshot.
|
|
18
|
+
- Added Value maps and label-specific shortlists with frozen percentiles, server-owned signals and explanations, and same-platform peer maps. Map points are returned with `include_map: true`; otherwise `map_points_omitted` reports their count. Ranking pages omit repeated capabilities.
|
|
19
|
+
- Preserved monetary decimal strings, payout bounds, zero/null distinctions, report populations, response definitions, source timestamps and authoritative activity summaries. Payout output schemas include the API's version identity and series fields.
|
|
20
|
+
- Added `opportunity_tiers` to rankings and every per-view alias. The any-of filter supports `elite`, `hot`, `strong`, and `potential`, uses tiers frozen into the Activity version, and is guarded by API capability negotiation.
|
|
21
|
+
- `find_programs` and `find_hunt_targets` combine server-side payout, scope, reward, update, report, response and status predicates with local discovery filters. Pages retain server order; `scan.complete` and `scan.next_cursor` report coverage and continue at the exact next row without mixing versions. Use these tools for combined Activity queries; `search_programs` and `get_opportunities` retain their catalog inputs.
|
|
22
|
+
- Negotiated catalog and Activity filter guards preserve canonical VDP modes and opportunity-kind filtering. Older or disabled APIs cannot silently drop explicit criteria. `find_hunt_targets` rejects unknown inputs, and each Activity tab rejects inputs belonging to another tab.
|
|
23
|
+
- Activity and combined catalog reads share the changes quota and two-request concurrency pool. Private responses are never retained in reusable caches. Activity and chronological failure circuits are isolated from healthy catalog and ranking reads.
|
|
24
|
+
- Reduced discovery overhead with open diagnostic envelope records, while preserving structured results and full-profile tools.
|
|
25
|
+
- Updated `@modelcontextprotocol/sdk` to 1.31.0 and refreshed patched transitive dependencies.
|
|
26
|
+
- Added fixture coverage for every Activity view, discovery response validation, exact identities, cursor and snapshot forwarding, no-store behavior, combined scans, capability guards and concurrency. Package-install smoke checks now exercise Activity and payouts; the deadline regression test accepts scheduling-dependent request counts within the shared budget.
|
|
27
|
+
|
|
3
28
|
## 0.2.6 — expanded Pro API
|
|
4
29
|
|
|
5
30
|
- Added seven direct tools for semantic History, exact program activity, and daily analytics, using one data request per page and lazy change detail.
|
package/README.md
CHANGED
|
@@ -135,6 +135,179 @@ Repository results preserve the API's comparison completeness/method/ancestry ev
|
|
|
135
135
|
|
|
136
136
|
`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
137
|
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
### Activity and payouts
|
|
141
|
+
|
|
142
|
+
Use `get_activity_capabilities` before choosing a ranking. Its versioned views give
|
|
143
|
+
the enabled metric, period, payout basis, precision, sort and direction together.
|
|
144
|
+
Each ranking requires validated values from at least three platforms before it is
|
|
145
|
+
enabled. User filters do not re-run that gate, so filtering a supported ranking to
|
|
146
|
+
one platform is valid. A 422 means the requested combination is unavailable; never
|
|
147
|
+
silently replace its period or basis.
|
|
148
|
+
|
|
149
|
+
| Tool | Purpose |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `get_activity_capabilities` | Enabled views, directions, label filters and listing readiness |
|
|
152
|
+
| `get_activity_snapshot` | Identity of the current published Activity version; no rows |
|
|
153
|
+
| `get_program_payouts` | One exact program's monetary, received-report and response facts |
|
|
154
|
+
| `get_activity_rankings` | One server-ranked page of any Activity view, selected with `tab` |
|
|
155
|
+
| `get_activity_change_detail` | One bounded page of an exact Activity event's before/after items |
|
|
156
|
+
|
|
157
|
+
`get_activity_rankings` takes `tab`: `payouts` (reported or tracked, the two bases
|
|
158
|
+
kept separate), `value` (frozen Value map and label-specific shortlists),
|
|
159
|
+
`scope`, `rewards`, `updates`, `reports`, `response`, `status` (lifecycle events;
|
|
160
|
+
first seen is not a launch date) or `all_changes` (catalog events, including
|
|
161
|
+
eligible events for departed programs). Each tab accepts only its own inputs:
|
|
162
|
+
`basis`/`precision` for payouts, `category` for updates and all changes,
|
|
163
|
+
`change_type` for status, `launched_within` for reports and response, and
|
|
164
|
+
`report_change_min`/`report_change_max` for reports. Anything else fails before a
|
|
165
|
+
request instead of reaching a different ranking. The `full` profile also keeps
|
|
166
|
+
single-view aliases (`get_activity_payouts`, `get_activity_value`,
|
|
167
|
+
`get_activity_scope`, `get_activity_rewards`, `get_activity_updates`,
|
|
168
|
+
`get_activity_reports`, `get_activity_response`, `get_activity_status` and
|
|
169
|
+
`get_activity_changes`) with the same request path.
|
|
170
|
+
|
|
171
|
+
Every tab also accepts `opportunity_tiers` (any of `elite`, `hot`, `strong`,
|
|
172
|
+
`potential`): the catalog opportunity tier frozen into the Activity version, so a
|
|
173
|
+
ranking can be narrowed to, for example, Elite and Hot programs.
|
|
174
|
+
`get_activity_capabilities` lists the tiers present in `facets.opportunity_tiers`.
|
|
175
|
+
An API that does not advertise the filter is told so before any request.
|
|
176
|
+
|
|
177
|
+
Ranking pages omit the capabilities document the API repeats on every page; read it
|
|
178
|
+
once with `get_activity_capabilities`. When a tab has no enabled default view the
|
|
179
|
+
empty page carries that tab's `available_views` instead.
|
|
180
|
+
|
|
181
|
+
`tab: "value"` has no period input. With `include_map: true` its `points` cover the
|
|
182
|
+
full filtered map population from the same snapshot (up to 5,000); by default the
|
|
183
|
+
map is replaced by `map_points_omitted` with its count, because thousands of points
|
|
184
|
+
rarely help a text answer. `rows` are a separately paginated shortlist. Percentiles stay frozen within their original platform cohorts after
|
|
185
|
+
filtering. Label-specific shortlists use their own criteria, so a small-cohort
|
|
186
|
+
“No payouts seen” program can appear without map percentiles. Keep its received
|
|
187
|
+
count and Tracked/Reported basis together. Explicit unsupported combinations are
|
|
188
|
+
rejected rather than replaced.
|
|
189
|
+
|
|
190
|
+
Rankings and `get_program_payouts` return server-owned `signals`, `labels` and
|
|
191
|
+
`label_details`, including explanations and destinations. Program detail also
|
|
192
|
+
returns `value_points` for same-platform peers when called with `include_map: true`
|
|
193
|
+
(otherwise `map_points_omitted` gives their count). “Last payout seen” describes an
|
|
194
|
+
observed counter increase, never a payment date. “Pays near top” is the platform's
|
|
195
|
+
top-quarter rule: retain the actual amount/percentage in the explanation. Processed
|
|
196
|
+
vs received can exceed 100% and does not measure response speed. Never recalculate
|
|
197
|
+
these signals or percentiles over one returned page.
|
|
198
|
+
|
|
199
|
+
For Activity event detail, supply the event ID and the source page's
|
|
200
|
+
`meta.snapshot_id` as `snapshot`. Keep that snapshot and page size with any detail
|
|
201
|
+
cursor. Legacy `get_history_change_detail` retains its existing History contract.
|
|
202
|
+
|
|
203
|
+
The ranking tools share `GET /v1/pro/activity/rankings`, fetch one bounded server
|
|
204
|
+
page and return the frozen `meta.as_of`, snapshot ID and opaque `next_cursor`.
|
|
205
|
+
On Activity 410, restart the same ranking without its cursor. For event details,
|
|
206
|
+
refresh the originating Activity page and reuse its new `meta.snapshot_id`.
|
|
207
|
+
Activity ranking tools do not accept `snapshot: "start"` or numeric `after_id`.
|
|
208
|
+
Activity is published in versions about every 30 seconds. `get_activity_snapshot`
|
|
209
|
+
reports the current version; pass its `snapshot_id` as `snapshot` to read
|
|
210
|
+
capabilities, a first page or program payouts from that exact version. Starting a
|
|
211
|
+
read on a version requires at least five minutes of remaining lifetime; an expired
|
|
212
|
+
or withdrawn version returns 410, after which you restart without the binding.
|
|
213
|
+
Never combine rows from different versions. `as_of` is when BBRadar froze its
|
|
214
|
+
calculation, not when a provider paid or updated anything.
|
|
215
|
+
Reuse the same filters, ordering and limit with that cursor. On expiry, restart the
|
|
216
|
+
query; do not merge different snapshots. No browser-like crawling, provider reads
|
|
217
|
+
or client-side global ranking is performed. Reads use the existing shared quota
|
|
218
|
+
and two-request concurrency pool, including per-program payout detail. Private
|
|
219
|
+
results and dynamic capabilities are never retained in reusable response caches.
|
|
220
|
+
|
|
221
|
+
For example, after confirming the combination is enabled, call
|
|
222
|
+
`get_activity_rankings` with
|
|
223
|
+
`{"tab":"payouts","period":"30d","basis":"tracked","precision":"exact","sort":"paid","direction":"desc","limit":25}`
|
|
224
|
+
for the largest measured increases in the **last 30 days**. The periods are rolling
|
|
225
|
+
windows, not calendar months. Resolve "lately" consistently for high and low payout
|
|
226
|
+
queries: prefer an enabled tracked 30-day comparison, or name a separately ranked
|
|
227
|
+
reported 90-day comparison. An explicit 7- or 30-day request never falls back to a
|
|
228
|
+
90-day or lifetime amount.
|
|
229
|
+
|
|
230
|
+
With `precision: "proven_bounds"`, descending means **Highest reported minimum**
|
|
231
|
+
using each value's lower bound; ascending means **Lowest reported maximum** using
|
|
232
|
+
its upper bound. Read `sort_directions` because these two orders qualify
|
|
233
|
+
independently. Keep `>`, `<`, interval notation, currency and observation dates in
|
|
234
|
+
the answer. A first baseline is not a payout, a counter increase is not a payment
|
|
235
|
+
date, and a hidden or ambiguous value is not zero.
|
|
236
|
+
|
|
237
|
+
Reports and resolved counters support `7d`, `30d`, `90d`, `all_time`, and API-only
|
|
238
|
+
`24h` where enabled. All-time uses the fresh lifetime counter; it has no Previous
|
|
239
|
+
or Change. Finite report comparisons retain the same basis within each program;
|
|
240
|
+
report volume does not establish researcher competition. Scope, Rewards, Updates,
|
|
241
|
+
Status and All changes support only `7d`, `30d` and `90d`. Response counts retain
|
|
242
|
+
the provider's term; time to bounty measures award timing, not settlement. Label
|
|
243
|
+
filters must come from current capabilities, and unsupported labels remain errors.
|
|
244
|
+
|
|
245
|
+
Use exact `platform:handle` identities for `get_program_payouts`; slash-containing
|
|
246
|
+
handles and literal percent signs are preserved. `null`, zero, bounds, report
|
|
247
|
+
definitions and source timestamps remain intact in structured output. Human
|
|
248
|
+
summaries should show the available numbers and units, keeping internal collection
|
|
249
|
+
and completeness metadata in the structured result.
|
|
250
|
+
|
|
251
|
+
`find_programs` and `find_hunt_targets` accept combined Activity predicates
|
|
252
|
+
(`search_programs` and `get_opportunities` reject them; use `find_programs`, with one
|
|
253
|
+
`opportunity_levels` entry for a tier). The API applies them together before
|
|
254
|
+
pagination, preserving one global ordering and one stable snapshot. For example, use
|
|
255
|
+
`{"payout_period":"30d","payout_metric":"payout_increase","payout_min":1000,"reports_period":"7d","reports_max":10,"sort_by":"payout_amount","sort_direction":"desc"}`
|
|
256
|
+
to combine measured payouts in the last 30 days with report activity in the last
|
|
257
|
+
7 days. Add `snapshot: "start"` on the first page and reuse `meta.snapshot` with
|
|
258
|
+
identical filters. The default order remains `updated` descending. Bounds sorts
|
|
259
|
+
require `proven_bounds`: `payout_lower_bound` descends and `payout_upper_bound`
|
|
260
|
+
ascends. `payout_positive: false` requires an exact observed zero; omit it when
|
|
261
|
+
both zero and positive payers should qualify.
|
|
262
|
+
|
|
263
|
+
Combined Activity queries require negotiated `activity_filters.enabled=true`.
|
|
264
|
+
Catalog-only program type, canonical VDP mode and `updated` ordering use the
|
|
265
|
+
separately advertised `catalog_filters` contract, so ordinary discovery remains
|
|
266
|
+
available when Activity is disabled. Server-ranked VDP choices are `include`,
|
|
267
|
+
`exclude` and `only`; legacy heuristic `only_likely` / `only_known_no_bounty`
|
|
268
|
+
choices remain in sampled discovery and are never silently mapped to a different
|
|
269
|
+
server predicate. Opportunity kind filters require advertised support and run
|
|
270
|
+
before pagination. Legacy search plus local-only discovery filters remains a
|
|
271
|
+
sampled workflow and is never labelled as a complete server ranking.
|
|
272
|
+
|
|
273
|
+
The combined inputs cover scope movement, reward changes, active days and change
|
|
274
|
+
counts, report levels and change, resolved counts, response timings, lifecycle
|
|
275
|
+
events, canonical launch cohorts and advertised activity labels. Give each
|
|
276
|
+
period-based signal its own period. `no_increase` and `lower` payout activity use
|
|
277
|
+
tracked 30-day values. All-time report comparisons, unsupported precision or
|
|
278
|
+
coverage states, automatic `recent` cohorts, `resolution_gap`, and
|
|
279
|
+
`opportunity_score` ordering are unavailable in this release; explicit unsupported
|
|
280
|
+
requests fail instead of silently substituting another metric.
|
|
281
|
+
|
|
282
|
+
With combined inputs alone, `find_programs` reads exactly one server page;
|
|
283
|
+
`max_results` sets its size and `page` continues it. Combined inputs also work
|
|
284
|
+
together with local filters such as `exclude_web3`, `min_wildcards`,
|
|
285
|
+
`min_eligible_targets`, bounty and report-count limits, `competition_risks` and
|
|
286
|
+
`free_submission_only` (no detected submission fee or deposit), plus target samples.
|
|
287
|
+
The MCP then reads full server pages in the server's order and the local filters
|
|
288
|
+
only remove programs, so the result is still a prefix of the global order. It reads
|
|
289
|
+
one page first and prefetches at most two more at a time when the observed match
|
|
290
|
+
rate shows they are needed. `scan.complete` says whether the whole server result
|
|
291
|
+
was examined; otherwise `scan.next_cursor` continues at the exact next row with the
|
|
292
|
+
same filters. A scan never mixes Activity versions: within one call it restarts
|
|
293
|
+
once on the new version, and a cursor from an older version fails with a restart
|
|
294
|
+
instruction.
|
|
295
|
+
|
|
296
|
+
For example, "a program with recent payouts, low competition, new scope, wide Web2
|
|
297
|
+
scope and no submission fee" is one `find_programs` call:
|
|
298
|
+
`{"payout_period":"30d","payout_positive":true,"scope_period":"30d","scope_added_min":1,"exclude_web3":true,"min_wildcards":1,"competition_risks":["Low"],"free_submission_only":true}`.
|
|
299
|
+
`find_hunt_targets` accepts the same Activity inputs (with `surface_mode: "web2"`
|
|
300
|
+
and `free_submission_only`) and returns ranked finalists with exact target samples.
|
|
301
|
+
Activity-filtered catalog reads count toward the API's shared changes quota, so the
|
|
302
|
+
MCP routes them through the same two-request concurrency pool as rankings and
|
|
303
|
+
change feeds.
|
|
304
|
+
|
|
305
|
+
It accepts at most one opportunity tier, and sampled sorts such as
|
|
306
|
+
`best_hunt_value` stay in legacy discovery, which still works when no combined
|
|
307
|
+
inputs are supplied. Do not form combined results by intersecting separately
|
|
308
|
+
paginated tools. VDP mode is applied on the server for combined queries,
|
|
309
|
+
preserving unknown reward types under `exclude`.
|
|
310
|
+
|
|
138
311
|
### Pro History and analytics
|
|
139
312
|
|
|
140
313
|
The `2026-09-24` API contract maps to these direct MCP tools. Each call reads one page unless the caller explicitly supplies a cursor for the next page. Change detail is fetched only when requested.
|
|
@@ -173,13 +346,29 @@ Already-retired historical events cannot be reconstructed. `get_program_targets`
|
|
|
173
346
|
follows upstream API pages until it can satisfy the requested offset/limit or the
|
|
174
347
|
inventory ends. Missing public scope is treated as out of scope.
|
|
175
348
|
|
|
349
|
+
Target lists expose `meta.has_more` and `meta.next_offset`; repeat the same filters
|
|
350
|
+
with that offset to continue. `language_tags` applies while scanning pages, so
|
|
351
|
+
sparse matches do not skip eligible targets. Named target actions honor their
|
|
352
|
+
target-type and language filters. `get_latest_added_targets` also pages its optional
|
|
353
|
+
inventory and returns `full_targets_next_offset` for `get_program_targets`.
|
|
354
|
+
Scope summaries and breakdowns label sampled counts with `count_scope` and
|
|
355
|
+
`possibly_incomplete`; unknown full filtered totals are omitted. Legacy export
|
|
356
|
+
fallbacks disclose their cap and reject filters whose upstream semantics they
|
|
357
|
+
cannot preserve. All tools reject unknown arguments, and malformed collection
|
|
358
|
+
responses return errors instead of claiming the inventory is empty.
|
|
359
|
+
|
|
360
|
+
Small direct target reads request only the prefix needed for their offset and limit,
|
|
361
|
+
up to the negotiated page size, instead of always loading a 500-row page. Local
|
|
362
|
+
scope and language post-filters retain full pages to find sparse matches. This
|
|
363
|
+
reduces excess transfer and processing while preserving fresh reads and row offsets.
|
|
364
|
+
|
|
176
365
|
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.
|
|
177
366
|
|
|
178
367
|
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.
|
|
179
368
|
|
|
180
369
|
## Performance Tuning
|
|
181
370
|
|
|
182
|
-
The defaults expose the
|
|
371
|
+
The defaults expose the 30-tool core surface and compact text results while enabling bounded concurrency, in-flight request coalescing, endpoint circuit breakers, and response caching only when upstream cache policy permits it. The Pro deployment currently sends `private, no-store`, so its responses are not placed in the reusable response cache or stale-while-revalidate cache. Set the tool profile to `full` when all specialized tools are needed.
|
|
183
372
|
|
|
184
373
|
| Environment variable | Default | Purpose |
|
|
185
374
|
| --- | ---: | --- |
|
|
@@ -189,7 +378,7 @@ The defaults expose the 25-tool core surface and compact text results while enab
|
|
|
189
378
|
| `BBRADAR_MCP_MAX_CONCURRENT_REQUESTS` | `6` | Maximum simultaneous upstream BBRadar API requests. |
|
|
190
379
|
| `BBRADAR_MCP_REQUEST_TIMEOUT_SECONDS` | `30` | Total upstream request deadline, including queue time. |
|
|
191
380
|
| `BBRADAR_MCP_COMPACT_TOOL_TEXT` | `true` | Avoid duplicating full results for clients that consume MCP `structuredContent`. Set to `false` for legacy text-only clients. |
|
|
192
|
-
| `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact
|
|
381
|
+
| `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact 30-tool discovery surface. Set to `full` for every specialized tool. |
|
|
193
382
|
| `BBRADAR_MCP_PROGRAM_STALE_WHILE_REVALIDATE_SECONDS` | `300` | Maximum stale window for exact program metadata while one background refresh runs. |
|
|
194
383
|
| `BBRADAR_MCP_CIRCUIT_BREAKER_FAILURE_THRESHOLD` | `3` | Consecutive retryable endpoint failures before its circuit opens. |
|
|
195
384
|
| `BBRADAR_MCP_CIRCUIT_BREAKER_COOLDOWN_SECONDS` | `30` | Endpoint circuit cooldown before a new request is admitted. |
|
|
@@ -208,6 +397,24 @@ Call `get_mcp_status` to inspect cache and stale-hit rates, circuit states, curs
|
|
|
208
397
|
|
|
209
398
|
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.
|
|
210
399
|
|
|
400
|
+
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.
|
|
401
|
+
|
|
402
|
+
`node benchmark/live-api-check.mjs` checks negotiated capabilities, discovery,
|
|
403
|
+
all nine Activity tabs, program and target reads, payouts, History, daily analytics,
|
|
404
|
+
feeds, combined filters and the same hunt. It emits aggregate measurements and
|
|
405
|
+
stops at the first failure. An optional section (`activity`, `programs`, `analytics`,
|
|
406
|
+
`feeds`, or `hunt`) runs only that workflow and its prerequisites, so independent
|
|
407
|
+
checks can continue without repeating successful sections. These are live reads
|
|
408
|
+
and consume quota; a smoke pass is not an uptime or complete-catalog guarantee.
|
|
409
|
+
|
|
410
|
+
`node benchmark/live-stdio.mjs` performs the original hunt through the actual
|
|
411
|
+
STDIO server entry point using the configured key, then compares a fresh small
|
|
412
|
+
target page with the prefix of a fresh wider page. Use `targets` to run the target
|
|
413
|
+
check independently. It reports cache activity, timings and counts, never target
|
|
414
|
+
identifiers. Partial hunts fail the benchmark instead of counting as speed gains.
|
|
415
|
+
`live-hunt.mjs --catalog-concurrency=2` can test a lower catalog request cap;
|
|
416
|
+
inspect both timings and coverage before adopting a concurrency change.
|
|
417
|
+
|
|
211
418
|
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.
|
|
212
419
|
|
|
213
420
|
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.
|
|
@@ -220,7 +427,13 @@ Persisted numeric feed checkpoints are scoped to the API destination and credent
|
|
|
220
427
|
|
|
221
428
|
Use `find_hunt_targets` as the default one-call path when a request combines good rewards, wide scope, Web2/Web3 constraints, competition, target quality, or recently added targets. It performs one catalog pass, fetches the bounded recent-additions feed in parallel, enriches only the finalists with exact in-scope bounty-eligible target samples, and returns a final compact ranking. Its result explicitly requires no follow-up calls; use `get_program_targets` only when the user asks for an exhaustive scope dump for one selected program.
|
|
222
429
|
|
|
223
|
-
`find_hunt_targets` supports arbitrary platform inclusion/exclusion, scope/target/language filters, reward and breadth thresholds, Web2/Web3 modes, target-intelligence filters, freshness windows, and balanced/reward/scope/freshness/competition ranking. It suppresses bulk `bbradar://` resource links while preserving each program's public BBRadar and platform URLs, preventing large shortlists from flooding model context.
|
|
430
|
+
`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.
|
|
431
|
+
|
|
432
|
+
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.
|
|
433
|
+
|
|
434
|
+
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.
|
|
435
|
+
|
|
436
|
+
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.
|
|
224
437
|
|
|
225
438
|
```json
|
|
226
439
|
{
|
|
@@ -269,6 +482,20 @@ This path uses one recent-changes request. Use `find_wildcard_programs` for a ra
|
|
|
269
482
|
- `get_history_change_detail`
|
|
270
483
|
- `get_program_history`
|
|
271
484
|
- `get_program_activity`
|
|
485
|
+
- `get_activity_capabilities`
|
|
486
|
+
- `get_activity_snapshot`
|
|
487
|
+
- `get_activity_rankings`
|
|
488
|
+
- `get_program_payouts`
|
|
489
|
+
- `get_activity_payouts`
|
|
490
|
+
- `get_activity_value`
|
|
491
|
+
- `get_activity_scope`
|
|
492
|
+
- `get_activity_rewards`
|
|
493
|
+
- `get_activity_updates`
|
|
494
|
+
- `get_activity_reports`
|
|
495
|
+
- `get_activity_response`
|
|
496
|
+
- `get_activity_status`
|
|
497
|
+
- `get_activity_changes`
|
|
498
|
+
- `get_activity_change_detail`
|
|
272
499
|
- `get_analytics_platform_series`
|
|
273
500
|
- `get_analytics_program_activity`
|
|
274
501
|
- `get_analytics_program_detail`
|
|
@@ -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
|
+
}
|