@bbradar/mcp 0.3.1 → 0.3.3
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 +13 -0
- package/README.md +47 -9
- package/dist/sanitize.js +37 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +90 -36
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { NumericCursorStore } from "./cursorStore.js";
|
|
|
7
7
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
8
8
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
9
9
|
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
10
|
-
const SERVER_VERSION = "0.3.
|
|
10
|
+
const SERVER_VERSION = "0.3.3";
|
|
11
11
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
12
12
|
const MAX_TARGET_EXPORT = 500;
|
|
13
13
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -46,7 +46,7 @@ const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
|
46
46
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
47
47
|
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
48
48
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
49
|
-
const SDK_VERSION = "1.
|
|
49
|
+
const SDK_VERSION = "1.32.2";
|
|
50
50
|
const CORE_TOOL_NAMES = new Set([
|
|
51
51
|
"search_programs",
|
|
52
52
|
"get_program",
|
|
@@ -294,7 +294,7 @@ const ACTIVITY_SHARED_PARAMS = ["tab", "sort", "direction", "platforms", "tags",
|
|
|
294
294
|
// a request so it cannot be silently ignored or reinterpreted.
|
|
295
295
|
const ACTIVITY_TAB_PARAMS = {
|
|
296
296
|
payouts: ["period", "basis", "precision"],
|
|
297
|
-
value: ["include_map"],
|
|
297
|
+
value: ["period", "include_map"],
|
|
298
298
|
scope: ["period"],
|
|
299
299
|
rewards: ["period"],
|
|
300
300
|
updates: ["period", "category"],
|
|
@@ -336,7 +336,7 @@ const activityCatalogInput = {
|
|
|
336
336
|
activity_labels: z.array(z.string().min(1).max(64)).max(10).optional(),
|
|
337
337
|
sort_by: activityCatalogSortSchema.optional(), sort_direction: z.enum(["asc", "desc"]).optional()
|
|
338
338
|
};
|
|
339
|
-
const activityCatalogGuidance = "Give each Activity signal its own period. payout_total requires all_time; payout_increase uses 24h/7d/30d/90d; payout_native_90d requires 90d. change_category requires updates_period even when scope_period is present; scope_period with scope_added_min needs no change_category. Use enabled sort_by/sort_direction combinations; default is updated descending. Tracked no_increase/lower require 30d. Catalog snapshot is 'start' or meta.snapshot, never an Activity snapshot_id. An empty result does not authorize relaxing the user's thresholds or restricting platforms to a recent-feed sample.";
|
|
339
|
+
const activityCatalogGuidance = "Give each Activity signal its own period. payout_total requires all_time; payout_increase uses 24h/7d/30d/90d; payout_native_90d requires 90d. change_category requires updates_period even when scope_period is present; scope_period with scope_added_min needs no change_category. Use enabled sort_by/sort_direction combinations; default is updated descending, and an omitted direction for first_response or time_to_bounty means ascending (fastest first). Tracked no_increase/lower require 30d. Catalog snapshot is 'start' or meta.snapshot, never an Activity snapshot_id. An empty result does not authorize relaxing the user's thresholds or restricting platforms to a recent-feed sample.";
|
|
340
340
|
const freeSubmissionOnlySchema = z.boolean().default(false).describe("Only programs with no detected submission fee or deposit.");
|
|
341
341
|
const programIdSchema = z
|
|
342
342
|
.string()
|
|
@@ -348,6 +348,7 @@ const programIdSchema = z
|
|
|
348
348
|
message: "program_id cannot contain control characters."
|
|
349
349
|
})
|
|
350
350
|
.describe("platform:handle id.");
|
|
351
|
+
const PROGRAM_IDENTIFIER_DESCRIPTION = "BBRadar platform:handle program_id or a human program name/handle. Names are resolved automatically.";
|
|
351
352
|
const programIdentifierSchema = z
|
|
352
353
|
.string()
|
|
353
354
|
.trim()
|
|
@@ -356,9 +357,9 @@ const programIdentifierSchema = z
|
|
|
356
357
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
357
358
|
message: "Program identifiers cannot contain control characters."
|
|
358
359
|
})
|
|
359
|
-
.describe(
|
|
360
|
+
.describe(PROGRAM_IDENTIFIER_DESCRIPTION);
|
|
360
361
|
function jsonRecordSchema() {
|
|
361
|
-
return z.record(z.unknown());
|
|
362
|
+
return z.record(z.string(), z.unknown());
|
|
362
363
|
}
|
|
363
364
|
function programOutputSchema() {
|
|
364
365
|
return jsonRecordSchema();
|
|
@@ -380,6 +381,13 @@ function apiEnvelopeOutputShape() {
|
|
|
380
381
|
error: jsonRecordSchema().optional()
|
|
381
382
|
};
|
|
382
383
|
}
|
|
384
|
+
// Tools that return one Pro API payload as it is. The API adds response fields
|
|
385
|
+
// between MCP releases, and clients validate each structured result against the
|
|
386
|
+
// discovered schema: listed fields document the contract, while a new top-level
|
|
387
|
+
// field validates instead of failing every call (the API added Status facets on 2026-10-13).
|
|
388
|
+
function apiMirrorOutput(shape) {
|
|
389
|
+
return z.object(shape).passthrough();
|
|
390
|
+
}
|
|
383
391
|
function programListOutputShape() {
|
|
384
392
|
return {
|
|
385
393
|
...apiEnvelopeOutputShape(),
|
|
@@ -517,8 +525,8 @@ export function createBbradarServer(client, config) {
|
|
|
517
525
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
518
526
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
519
527
|
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
520
|
-
const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
|
|
521
|
-
const analyticsOutput = { ...apiEnvelopeOutputShape(), platform: z.string().optional(), program: jsonRecordSchema().optional(), programs: z.array(jsonRecordSchema()).optional(), series: z.array(jsonRecordSchema()).optional(), observations: jsonRecordSchema().optional(), category: z.string().optional(), population: z.string().optional(), ranking_definition: z.string().optional(), inventory_definition: z.string().optional(), meta: jsonRecordSchema().optional() };
|
|
528
|
+
const historyOutput = apiMirrorOutput({ ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() });
|
|
529
|
+
const analyticsOutput = apiMirrorOutput({ ...apiEnvelopeOutputShape(), platform: z.string().optional(), program: jsonRecordSchema().optional(), programs: z.array(jsonRecordSchema()).optional(), series: z.array(jsonRecordSchema()).optional(), observations: jsonRecordSchema().optional(), category: z.string().optional(), population: z.string().optional(), ranking_definition: z.string().optional(), inventory_definition: z.string().optional(), meta: jsonRecordSchema().optional() });
|
|
522
530
|
// Published Activity versions are 64 lowercase hex characters. "start" and
|
|
523
531
|
// catalog snapshot tokens are different contracts and are rejected here.
|
|
524
532
|
const activitySnapshotSchema = z.string().regex(/^[0-9a-f]{64}$/, "Use a snapshot_id returned by Activity.")
|
|
@@ -537,7 +545,7 @@ export function createBbradarServer(client, config) {
|
|
|
537
545
|
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
538
546
|
search: z.string().min(1).max(200).optional(),
|
|
539
547
|
current_status: z.enum(["active", "paused", "suspended", "disabled", "closed", "unavailable", "unknown"]).optional(),
|
|
540
|
-
labels: z.array(z.string().min(1).max(64)).max(10).optional().describe("Only labels listed by get_activity_capabilities; others return 422."),
|
|
548
|
+
labels: z.array(z.string().min(1).max(64)).max(10).optional().describe("Only labels listed by get_activity_capabilities (for tab=value, the chosen period view's labels); others return 422."),
|
|
541
549
|
opportunity_tiers: z.array(opportunityLevelSchema).min(1).max(4).optional().describe("Any of these catalog opportunity tiers, as frozen into the Activity version. get_activity_capabilities lists the tiers present in facets.opportunity_tiers; an API without the filter is rejected before any request."),
|
|
542
550
|
launched_within: z.enum(["7d", "30d", "90d"]).optional(),
|
|
543
551
|
category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
@@ -547,19 +555,23 @@ export function createBbradarServer(client, config) {
|
|
|
547
555
|
cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(25),
|
|
548
556
|
snapshot: activitySnapshotSchema.optional()
|
|
549
557
|
};
|
|
550
|
-
const activityOutput = {
|
|
558
|
+
const activityOutput = apiMirrorOutput({
|
|
551
559
|
...apiEnvelopeOutputShape(), rows: z.array(jsonRecordSchema()).optional(), points: z.array(jsonRecordSchema()).optional(),
|
|
552
560
|
tab: z.string().optional(), period: z.string().nullable().optional(),
|
|
553
561
|
basis: z.string().nullable().optional(), precision: z.string().nullable().optional(),
|
|
554
562
|
sort: z.string().nullable().optional(), direction: z.string().optional(), meta: jsonRecordSchema().optional(),
|
|
555
563
|
cohort_median: jsonRecordSchema().nullable().optional(), available_views: z.array(jsonRecordSchema()).optional(),
|
|
556
|
-
map_points_omitted: z.number().int().optional()
|
|
557
|
-
|
|
564
|
+
map_points_omitted: z.number().int().optional(),
|
|
565
|
+
// tab=value: the label shortlist applied, including a default landing list (empty = the map's programs).
|
|
566
|
+
labels: z.array(z.string()).optional(),
|
|
567
|
+
// tab=status: event counts per change_type and current_status, each under the other filters.
|
|
568
|
+
facets: jsonRecordSchema().optional()
|
|
569
|
+
});
|
|
558
570
|
registerTool("get_activity_capabilities", {
|
|
559
571
|
title: "Get Available Activity Views",
|
|
560
572
|
description: "Read the exact enabled period, sort, direction, basis and precision combinations before choosing an Activity ranking. Availability is evaluated before user filters; do not infer combinations from independent enums. Honor each view's sort_directions and default_direction and the enabled label list. The snapshot block identifies the published version these capabilities belong to.",
|
|
561
573
|
inputSchema: z.object({ snapshot: activitySnapshotSchema.optional() }).strict(),
|
|
562
|
-
outputSchema: { ...apiEnvelopeOutputShape(), tabs: z.array(jsonRecordSchema()).optional(), version: z.union([z.string(), z.number()]).optional(), facets: jsonRecordSchema().optional(), labels: z.array(z.string()).optional(), listing: jsonRecordSchema().optional(), listing_payouts_enabled: z.boolean().optional(), derived_signals_enabled: z.boolean().optional(), snapshot: jsonRecordSchema().optional() },
|
|
574
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), tabs: z.array(jsonRecordSchema()).optional(), version: z.union([z.string(), z.number()]).optional(), facets: jsonRecordSchema().optional(), labels: z.array(z.string()).optional(), listing: jsonRecordSchema().optional(), listing_payouts_enabled: z.boolean().optional(), derived_signals_enabled: z.boolean().optional(), snapshot: jsonRecordSchema().optional() }),
|
|
563
575
|
annotations: readOnlyAnnotations
|
|
564
576
|
}, (args) => runTool("get_activity_capabilities", config, rateLimiter, async () => {
|
|
565
577
|
assertProFeature(client, "activity_capabilities", "enabled");
|
|
@@ -570,7 +582,7 @@ export function createBbradarServer(client, config) {
|
|
|
570
582
|
title: "Get Current Activity Version",
|
|
571
583
|
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds. Pass snapshot_id as snapshot only to get_activity_capabilities, get_activity_rankings and get_program_payouts. Catalog/feed tools such as find_programs use snapshot=start then meta.snapshot instead. as_of is when BBRadar froze its calculation, not a provider payment or update time.",
|
|
572
584
|
inputSchema: z.object({}).strict(),
|
|
573
|
-
outputSchema: { ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() },
|
|
585
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() }),
|
|
574
586
|
annotations: readOnlyAnnotations
|
|
575
587
|
}, () => runTool("get_activity_snapshot", config, rateLimiter, async () => {
|
|
576
588
|
assertProFeature(client, "activity_snapshot", "enabled");
|
|
@@ -583,11 +595,11 @@ export function createBbradarServer(client, config) {
|
|
|
583
595
|
}));
|
|
584
596
|
registerTool("get_program_payouts", {
|
|
585
597
|
title: "Get Program Payouts and Response Facts",
|
|
586
|
-
description: "Read reported totals, measured counter increases, source dates, precision, report and response facts, derived signals and label explanations for one exact platform:handle. Keep null separate from zero, bounds separate from exact amounts and unknown payment dates null. Payout timestamps are when increases were seen, never payment dates. No provider requests are made.",
|
|
598
|
+
description: "Read reported totals, measured counter increases, source dates, precision, report and response facts, derived signals and label explanations for one exact platform:handle. facts also carry each platform's own published figures (paid_average, paid_maximum, last_paid_on, HackerOne per-severity averages and report counts, triage/resolution times, response rate, researcher counts), each with its measure, unit, period and source_label; keep each definition and never compare them across platforms as one measure. Keep null separate from zero, bounds separate from exact amounts and unknown payment dates null. Payout timestamps are when increases were seen, never payment dates. value_points (include_map) are same-platform peers in the Value period named by value_period. No provider requests are made.",
|
|
587
599
|
inputSchema: z.object({ program_id: programIdSchema, snapshot: activitySnapshotSchema.optional(), include_map: includeMapSchema }).strict(),
|
|
588
|
-
//
|
|
600
|
+
// Lists the API response fields, including its version identity: a client that
|
|
589
601
|
// discovered this schema validates every structured result against it.
|
|
590
|
-
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), program: jsonRecordSchema().optional(), summary: jsonRecordSchema().optional(), as_of: z.string().optional(), snapshot_id: z.string().optional(), expires_at: z.string().nullable().optional(), facts: z.array(jsonRecordSchema()).optional(), windows: jsonRecordSchema().optional(), series: z.array(jsonRecordSchema()).optional(), payout_summary: jsonRecordSchema().nullable().optional(), views: z.array(jsonRecordSchema()).optional(), reports: jsonRecordSchema().optional(), response: jsonRecordSchema().optional(), last_increase_at: z.string().nullable().optional(), signals: jsonRecordSchema().optional(), label_details: z.array(jsonRecordSchema()).optional(), labels: z.array(z.string()).optional(), value_points: z.array(jsonRecordSchema()).optional(), map_points_omitted: z.number().int().optional(), meta: jsonRecordSchema().optional() },
|
|
602
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), program_id: z.string().optional(), program: jsonRecordSchema().optional(), summary: jsonRecordSchema().optional(), as_of: z.string().optional(), snapshot_id: z.string().optional(), expires_at: z.string().nullable().optional(), facts: z.array(jsonRecordSchema()).optional(), windows: jsonRecordSchema().optional(), series: z.array(jsonRecordSchema()).optional(), payout_summary: jsonRecordSchema().nullable().optional(), views: z.array(jsonRecordSchema()).optional(), reports: jsonRecordSchema().optional(), response: jsonRecordSchema().optional(), last_increase_at: z.string().nullable().optional(), signals: jsonRecordSchema().optional(), label_details: z.array(jsonRecordSchema()).optional(), labels: z.array(z.string()).optional(), value_points: z.array(jsonRecordSchema()).optional(), value_period: z.string().nullable().optional(), map_points_omitted: z.number().int().optional(), meta: jsonRecordSchema().optional() }),
|
|
591
603
|
annotations: readOnlyAnnotations
|
|
592
604
|
}, (args) => runTool("get_program_payouts", config, rateLimiter, async () => {
|
|
593
605
|
assertProFeature(client, "program_payouts", "enabled");
|
|
@@ -606,7 +618,7 @@ export function createBbradarServer(client, config) {
|
|
|
606
618
|
};
|
|
607
619
|
registerTool("get_activity_rankings", {
|
|
608
620
|
title: "Get Activity Ranking",
|
|
609
|
-
description: "One globally ranked server page of an Activity view, frozen at meta.as_of. Read get_activity_capabilities first: explicit period/basis/precision/sort/direction never fall back, and each tab only accepts its own inputs. tab: payouts (reported and tracked bases stay separate; proven_bounds desc = highest reported minimum, asc = lowest reported maximum, never an exact ranking; source totals can lag); value (
|
|
621
|
+
description: "One globally ranked server page of an Activity view, frozen at meta.as_of. Read get_activity_capabilities first: explicit period/basis/precision/sort/direction never fall back, and each tab only accepts its own inputs. tab: payouts (reported and tracked bases stay separate; proven_bounds desc = highest reported minimum, asc = lowest reported maximum, never an exact ranking; source totals can lag; average_paid is the platform's own current average payment, a range for HackerOne); value (advertised period 7d/30d/90d of report volume against lifetime payout per processed report; each row's value holds that period's count and percentile; Pays near top, No payouts seen and processed_received are 90d only; without period, labels or sort mean 90d and a bare read opens the advertised default, whose shortlist the page's labels name; one label gives its shortlist); scope (recorded additions/removals/net, not inventory differences); rewards (net reward-range change comparing native maxima, unknown % stays null; minimum_usd/maximum_usd are display conversions); updates (active UTC days, change groups); reports (received counts; volume is not competition; all_time is the lifetime counter); response (resolved counts, last resolution, response time with each provider's own label and window; time to bounty is award timing; durations default to fastest first); status (first_seen/paused/resumed/left_listing/returned events; facets count events per change_type and current_status); all_changes (chronological events; get_activity_change_detail for before/after). Event tabs use rolling 7d/30d/90d, not calendar months. launched_within applies to reports/response. Payout increases are observed counters, not payment dates. Reuse cursors only with identical inputs. For several signals at once, or with Web2/scope/competition/fee filters, use find_programs or find_hunt_targets.",
|
|
610
622
|
inputSchema: z.object(activityParams).strict(),
|
|
611
623
|
outputSchema: activityOutput,
|
|
612
624
|
annotations: readOnlyAnnotations
|
|
@@ -617,14 +629,14 @@ export function createBbradarServer(client, config) {
|
|
|
617
629
|
// Single-view aliases for the full profile; each schema exposes only the
|
|
618
630
|
// inputs its view accepts and shares the ranking tool's request path.
|
|
619
631
|
const activityDescriptions = {
|
|
620
|
-
value: "Discover programs with frozen within-platform value and report-volume percentiles. Generosity is lifetime; volume
|
|
621
|
-
payouts: "Rank payouts using the same server calculation as BBRadar. Last 7 days and last 30 days are rolling windows. Explicit periods never fall back. For high or low payouts lately, use the same enabled tracked-30-day cohort, or separately label a native-90-day cohort. Reported and tracked bases stay separate. With proven_bounds, descending is Highest reported minimum using lower bounds; ascending is Lowest reported maximum using upper bounds. Neither claims an exact ranking. Source totals can be delayed, including Immunefi.",
|
|
632
|
+
value: "Discover programs with frozen within-platform value and report-volume percentiles. Generosity is lifetime payout per processed report; volume covers the selected period: 7d, 30d or 90d, each only when get_activity_capabilities advertises it. Each row's value holds that period's received count, basis and percentile; signals keep the lifetime and 90-day figures. Without period, naming labels or sort means 90d, and a bare read opens the advertised default, which may be a shortlist named in the page's labels. No launch-cohort input applies. High value, Popular and Low per report exist per period; Pays near top (orders by realization), No payouts seen (orders by received_90d, raw count with its basis; small cohorts remain eligible) and processed_received are 90d only. Comparative ratio sorts use server percentiles; never recompute after filtering. Preserve label_details explanations and destinations. A counter increase is observed activity, not an individual payment. Map points are returned only with include_map.",
|
|
633
|
+
payouts: "Rank payouts using the same server calculation as BBRadar. Last 7 days and last 30 days are rolling windows. Explicit periods never fall back. For high or low payouts lately, use the same enabled tracked-30-day cohort, or separately label a native-90-day cohort. Reported and tracked bases stay separate. With proven_bounds, descending is Highest reported minimum using lower bounds; ascending is Lowest reported maximum using upper bounds. Neither claims an exact ranking. Source totals can be delayed, including Immunefi. Rows also carry average_paid, the platform's own current average payment with its source_label, period, native currency and USD conversion (HackerOne publishes a 45th-55th percentile range); it is the same in every period, and sort=average_paid ranks it where advertised.",
|
|
622
634
|
scope: "Rank recorded scope additions, removals, net movement or largest update; not differences between inventory totals. Periods are rolling 7, 30 or 90 days, not calendar months.",
|
|
623
|
-
rewards: "Rank comparable net reward-range changes over rolling 7, 30 or 90 days.
|
|
635
|
+
rewards: "Rank comparable net reward-range changes over rolling 7, 30 or 90 days. The change percentage compares native maxima; unknown percentages remain null. before/after keep native amounts and add minimum_usd/maximum_usd at the version's current quotes for display (null without a quote).",
|
|
624
636
|
updates: "Rank active UTC days and recorded change groups over rolling periods. Baselines and internal corrections are excluded.",
|
|
625
637
|
reports: "Rank received report counts. Each program uses measured lifetime-counter growth where available, otherwise its supported native period. All-time is the current lifetime counter. Previous and change compare like-for-like. Report volume is not competition. Launch cohorts use the canonical launch date; amounts are not reports since launch.",
|
|
626
|
-
response: "Rank resolved counter growth or current lifetime counters, last resolution, response duration or time to bounty.
|
|
627
|
-
status: "Read distinct First seen, Paused, Resumed, Left listing and Returned events. Event type and current status are separate filters; never merge events from one program.",
|
|
638
|
+
response: "Rank resolved counter growth or current lifetime counters, last resolution, response duration or time to bounty. Each cell keeps its provider's definition, label and window (for example HackerOne first response over 90 days, YesWeHack's 6-month band, Bugcrowd's 75% validated time); first_response_precision says whether a time is exact, a band bound or approximate. Time to bounty is award timing, not payment settlement. Durations sort fastest first unless a direction is given. Period applies only to resolved counts; use only advertised periods.",
|
|
639
|
+
status: "Read distinct First seen, Paused, Resumed, Left listing and Returned events. Event type and current status are separate filters; never merge events from one program. facets count events per change_type and current_status for the period and filters, each counted without its own selection; offer only options with counts.",
|
|
628
640
|
all_changes: "Read chronological semantic program changes with event IDs. Fetch get_activity_change_detail lazily for before/after items, including eligible events for programs that left the catalog. Separate events stay separate."
|
|
629
641
|
};
|
|
630
642
|
for (const tab of ACTIVITY_TABS) {
|
|
@@ -643,7 +655,7 @@ export function createBbradarServer(client, config) {
|
|
|
643
655
|
title: "Get Activity Change Detail",
|
|
644
656
|
description: "Read one bounded page of recorded before/after items for an exact Activity event ID in its source snapshot. Pass meta.snapshot_id from the Activity page. Retained eligible events may describe programs that have left the catalog. Reuse an opaque cursor only with the same event, snapshot and limit.",
|
|
645
657
|
inputSchema: z.object({ event_id: z.string().min(1).max(128), snapshot: activitySnapshotSchema.describe("meta.snapshot_id from the Activity page that supplied this event."), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) }).strict(),
|
|
646
|
-
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
658
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
647
659
|
}, (args) => runTool("get_activity_change_detail", config, rateLimiter, async () => {
|
|
648
660
|
assertProFeature(client, "activity_event_detail", "enabled");
|
|
649
661
|
return proPage(await client.getActivityChangeDetail(args.event_id, toQuery({ snapshot: args.snapshot, cursor: args.cursor, limit: args.limit })), "items");
|
|
@@ -668,7 +680,7 @@ export function createBbradarServer(client, config) {
|
|
|
668
680
|
title: "Get History Change Detail",
|
|
669
681
|
description: "Fetch one semantic History change's paginated before/after items only when needed.",
|
|
670
682
|
inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
671
|
-
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
683
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
672
684
|
}, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
|
|
673
685
|
assertProFeature(client, "history_change_detail");
|
|
674
686
|
assertHistoryCapture(client);
|
|
@@ -688,7 +700,7 @@ export function createBbradarServer(client, config) {
|
|
|
688
700
|
title: "Get Exact Program Activity",
|
|
689
701
|
description: "Read the authoritative activity_summary for one exact program ID.",
|
|
690
702
|
inputSchema: { program_id: programIdSchema },
|
|
691
|
-
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
703
|
+
outputSchema: apiMirrorOutput({ ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }), annotations: readOnlyAnnotations
|
|
692
704
|
}, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
|
|
693
705
|
assertProFeature(client, "program_activity");
|
|
694
706
|
return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
|
|
@@ -795,7 +807,7 @@ export function createBbradarServer(client, config) {
|
|
|
795
807
|
}));
|
|
796
808
|
registerTool("get_program", {
|
|
797
809
|
title: "Get BBRadar Program",
|
|
798
|
-
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
|
|
810
|
+
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous. A contest or Huntr challenge adds contest_awards: its final published award allocations (awarded_total and pool_amount as decimal strings in the contest's own currency token; currency null when the platform names none, as Huntr; never USD or a payment record), or null before a final result. A finished contest with a final result stays readable here by exact platform:handle, without an opportunity tag, although discovery and name resolution exclude ended contests.",
|
|
799
811
|
inputSchema: {
|
|
800
812
|
program_id: programIdentifierSchema
|
|
801
813
|
},
|
|
@@ -1440,7 +1452,8 @@ export function createBbradarServer(client, config) {
|
|
|
1440
1452
|
min_added_7d: z.number().int().min(0).optional(),
|
|
1441
1453
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1442
1454
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
1443
|
-
|
|
1455
|
+
// One flat enum, as clients have always seen it; a union would advertise anyOf.
|
|
1456
|
+
sort_by: z.enum([...programSortSchema.removeDefault().options, ...activityCatalogSortSchema.options]).optional(),
|
|
1444
1457
|
include_target_samples: z.boolean().default(false),
|
|
1445
1458
|
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1446
1459
|
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
@@ -2398,6 +2411,7 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2398
2411
|
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Choose an enabled period, sort and direction, then call get_activity_rankings with that tab for one server-ranked page. Do not approximate by paging or re-ranking the catalog." },
|
|
2399
2412
|
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" }, then: "Use find_programs with the same Activity inputs and competition_risks for program-only results; continue with scan.next_cursor." },
|
|
2400
2413
|
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2414
|
+
{ user_goal: "final award results of a finished contest or Huntr challenge", first_tool: "get_program", arguments: { program_id: "Platform:handle" }, then: "Read contest_awards: awarded_total and pool_amount are decimal strings in the contest's own token (currency null when the platform names none); null means no final result yet." },
|
|
2401
2415
|
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch get_history_change_detail only for a selected change." },
|
|
2402
2416
|
{ user_goal: "most active programs in a date range", first_tool: "get_analytics_program_activity", arguments: {}, then: "Use this server-side ranking rather than sampling the program catalog." },
|
|
2403
2417
|
{ user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
@@ -2547,6 +2561,7 @@ function coreBbradarGuide(topic, client) {
|
|
|
2547
2561
|
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Read supported view combinations and call get_activity_rankings with that tab. Use get_activity_change_detail lazily for a selected Activity event." },
|
|
2548
2562
|
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" } },
|
|
2549
2563
|
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2564
|
+
{ user_goal: "final award results of a finished contest or Huntr challenge", first_tool: "get_program", arguments: { program_id: "Platform:handle" }, then: "Read contest_awards in the contest's own token; null means no final result yet." },
|
|
2550
2565
|
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
|
|
2551
2566
|
{ user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
|
|
2552
2567
|
{ user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
@@ -2818,7 +2833,7 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
2818
2833
|
return withProgramIdFallbackMetadata(payload, cachedFallback);
|
|
2819
2834
|
}
|
|
2820
2835
|
catch (cachedFallbackError) {
|
|
2821
|
-
if (!
|
|
2836
|
+
if (!allowsProgramIdFallback(cachedFallbackError) || !indexFallbackCallback) {
|
|
2822
2837
|
throw cachedFallbackError;
|
|
2823
2838
|
}
|
|
2824
2839
|
const indexFallback = createProgramIndexFallbackFromStaleResolution(cachedFallback, requestedProgramId, cachedFallbackError) ??
|
|
@@ -2840,7 +2855,7 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
2840
2855
|
return withProgramIdFallbackMetadata(payload, fallback);
|
|
2841
2856
|
}
|
|
2842
2857
|
catch (fallbackError) {
|
|
2843
|
-
if (!
|
|
2858
|
+
if (!allowsProgramIdFallback(fallbackError) || !indexFallbackCallback) {
|
|
2844
2859
|
throw fallbackError;
|
|
2845
2860
|
}
|
|
2846
2861
|
const indexFallback = createProgramIndexFallbackFromStaleResolution(fallback, requestedProgramId, fallbackError) ??
|
|
@@ -2868,7 +2883,7 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
2868
2883
|
return await callback(requestedProgramId);
|
|
2869
2884
|
}
|
|
2870
2885
|
catch (error) {
|
|
2871
|
-
if (!
|
|
2886
|
+
if (!allowsProgramIdFallback(error)) {
|
|
2872
2887
|
throw error;
|
|
2873
2888
|
}
|
|
2874
2889
|
const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
|
|
@@ -2890,7 +2905,7 @@ async function runProgramIdTool(client, config, requestedProgramId, callback, in
|
|
|
2890
2905
|
return withProgramIdFallbackMetadata(payload, fallback);
|
|
2891
2906
|
}
|
|
2892
2907
|
catch (fallbackError) {
|
|
2893
|
-
if (!
|
|
2908
|
+
if (!allowsProgramIdFallback(fallbackError) || !indexFallbackCallback) {
|
|
2894
2909
|
throw fallbackError;
|
|
2895
2910
|
}
|
|
2896
2911
|
const indexFallback = createProgramIndexFallbackFromStaleResolution(fallback, requestedProgramId, fallbackError) ??
|
|
@@ -5142,7 +5157,20 @@ async function getProgramFromTargetsOnlyFallback(client, programId, programError
|
|
|
5142
5157
|
async function fetchProgramDetailsAndTargets(client, config, programId) {
|
|
5143
5158
|
const [programResult, targetsResult] = await Promise.allSettled([client.getProgram(programId), client.getProgramTargets(programId)]);
|
|
5144
5159
|
if (targetsResult.status === "rejected") {
|
|
5145
|
-
|
|
5160
|
+
const reason = targetsResult.reason;
|
|
5161
|
+
if (programResult.status === "fulfilled" && isProgramNotFoundError(reason)) {
|
|
5162
|
+
// Detail confirmed this exact program; only its target list is unavailable.
|
|
5163
|
+
throw new ConfirmedProgramTargetsError({
|
|
5164
|
+
message: `${programId} exists, but its target list is not available (${reason.message}). A finished contest keeps program detail only: read it with get_program.`,
|
|
5165
|
+
requestId: reason.requestId,
|
|
5166
|
+
upstreamRequestId: reason.upstreamRequestId,
|
|
5167
|
+
status: reason.status,
|
|
5168
|
+
detail: reason.detail,
|
|
5169
|
+
errors: reason.errors,
|
|
5170
|
+
cause: reason
|
|
5171
|
+
});
|
|
5172
|
+
}
|
|
5173
|
+
throw reason;
|
|
5146
5174
|
}
|
|
5147
5175
|
const targetsApi = targetsResult.value;
|
|
5148
5176
|
const targetSourceRequest = {
|
|
@@ -6292,6 +6320,7 @@ function compactProgram(program) {
|
|
|
6292
6320
|
competition_risk: stringField(program, "competition_risk"),
|
|
6293
6321
|
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
6294
6322
|
submission_cost: program.submission_cost,
|
|
6323
|
+
contest_awards: program.contest_awards,
|
|
6295
6324
|
payout_summary: program.payout_summary,
|
|
6296
6325
|
activity: program.activity,
|
|
6297
6326
|
activity_summary: program.activity_summary,
|
|
@@ -7825,6 +7854,17 @@ function serverCatalogQuery(client, input) {
|
|
|
7825
7854
|
vdp_mode: input.vdp_mode, snapshot: input.snapshot }
|
|
7826
7855
|
};
|
|
7827
7856
|
}
|
|
7857
|
+
// Durations rank fastest first when no direction is given, as the API does. The
|
|
7858
|
+
// direction the server reports it applied wins, so an older API is labelled
|
|
7859
|
+
// correctly too.
|
|
7860
|
+
const ASCENDING_FIRST_SORTS = new Set(["first_response", "time_to_bounty"]);
|
|
7861
|
+
function appliedSortDirection(input, served) {
|
|
7862
|
+
if (served === "asc" || served === "desc")
|
|
7863
|
+
return served;
|
|
7864
|
+
if (input.sort_direction === "asc" || input.sort_direction === "desc")
|
|
7865
|
+
return input.sort_direction;
|
|
7866
|
+
return ASCENDING_FIRST_SORTS.has(String(input.sort_by ?? "updated")) ? "asc" : "desc";
|
|
7867
|
+
}
|
|
7828
7868
|
function readServerCatalogPage(client, catalog, page, pageSize, snapshot) {
|
|
7829
7869
|
const query = toQuery({ ...catalog.query, snapshot, page, page_size: pageSize });
|
|
7830
7870
|
return catalog.level ? client.getOpportunities(catalog.level, query) : client.listPrograms(query);
|
|
@@ -7841,7 +7881,7 @@ async function findServerCatalogPrograms(client, config, input) {
|
|
|
7841
7881
|
const mode = input.output_mode === "full" ? "full" : "compact";
|
|
7842
7882
|
return withApiMetadata(api, {
|
|
7843
7883
|
programs: readArray(data?.programs).map((program) => formatProgram(sanitizeProgram(program, config.webBaseUrl), mode)),
|
|
7844
|
-
ranking_scope: { mode: "server", sort_by: input.sort_by ?? "updated", sort_direction: input
|
|
7884
|
+
ranking_scope: { mode: "server", sort_by: input.sort_by ?? "updated", sort_direction: appliedSortDirection(input, readObject(data?.meta)?.sort_direction) },
|
|
7845
7885
|
meta: sanitizeJson(data?.meta)
|
|
7846
7886
|
});
|
|
7847
7887
|
}
|
|
@@ -7894,6 +7934,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7894
7934
|
let token = start.t ?? input.snapshot;
|
|
7895
7935
|
let totalPages;
|
|
7896
7936
|
let serverTotal;
|
|
7937
|
+
let servedDirection;
|
|
7897
7938
|
let rowsScanned = 0;
|
|
7898
7939
|
let pagesRead = 0;
|
|
7899
7940
|
let restarted = false;
|
|
@@ -7952,6 +7993,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7952
7993
|
version ??= pageVersion;
|
|
7953
7994
|
totalPages = readNumber(meta?.total_pages) ?? totalPages;
|
|
7954
7995
|
serverTotal = readNumber(meta?.total) ?? serverTotal;
|
|
7996
|
+
servedDirection = meta?.sort_direction ?? servedDirection;
|
|
7955
7997
|
if (token !== undefined && typeof meta?.snapshot === "string")
|
|
7956
7998
|
token = meta.snapshot;
|
|
7957
7999
|
const rows = readArray(data?.programs);
|
|
@@ -7987,7 +8029,7 @@ async function scanServerCatalog(client, config, input, local, limits) {
|
|
|
7987
8029
|
scan: stripUndefined({
|
|
7988
8030
|
order: "Server order; local filters only remove programs.",
|
|
7989
8031
|
sort_by: input.sort_by ?? "updated",
|
|
7990
|
-
sort_direction: input
|
|
8032
|
+
sort_direction: appliedSortDirection(input, servedDirection),
|
|
7991
8033
|
local_filters: localFilters.length > 0 ? localFilters : undefined,
|
|
7992
8034
|
server_matches: serverTotal,
|
|
7993
8035
|
rows_scanned: rowsScanned,
|
|
@@ -8906,6 +8948,16 @@ function addUniqueWarning(warnings, warning) {
|
|
|
8906
8948
|
function isProgramNotFoundError(error) {
|
|
8907
8949
|
return error instanceof BBRadarApiError && error.status === 404;
|
|
8908
8950
|
}
|
|
8951
|
+
/**
|
|
8952
|
+
* A target read failed for a program whose exact detail read succeeded. Detail confirmed
|
|
8953
|
+
* the identity, so the failure belongs to that program's targets (a finished contest keeps
|
|
8954
|
+
* detail only) and never makes its ID a stale-ID candidate for a same-handle program.
|
|
8955
|
+
*/
|
|
8956
|
+
class ConfirmedProgramTargetsError extends BBRadarApiError {
|
|
8957
|
+
}
|
|
8958
|
+
function allowsProgramIdFallback(error) {
|
|
8959
|
+
return isProgramNotFoundError(error) && !(error instanceof ConfirmedProgramTargetsError);
|
|
8960
|
+
}
|
|
8909
8961
|
function runImmediately(callback) {
|
|
8910
8962
|
return callback();
|
|
8911
8963
|
}
|
|
@@ -9055,7 +9107,9 @@ Summarize the program scope:
|
|
|
9055
9107
|
title: "Prepare Passive Recon Plan",
|
|
9056
9108
|
description: "Create a passive-only plan from BBRadar program and target data. Use only when planning is explicitly requested.",
|
|
9057
9109
|
argsSchema: {
|
|
9058
|
-
|
|
9110
|
+
// Zod 4 keeps a description on the schema it was set on, and prompt
|
|
9111
|
+
// arguments read the outer optional, so describe it again here.
|
|
9112
|
+
program_id: programIdentifierSchema.optional().describe(PROGRAM_IDENTIFIER_DESCRIPTION),
|
|
9059
9113
|
focus: z.string().optional().describe("Optional focus area such as APIs, domains, mobile, or cloud.")
|
|
9060
9114
|
}
|
|
9061
9115
|
}, (args) => promptResult(`Prepare a passive-only plan from BBRadar data. Program id JSON string, if present: ${promptJson(args.program_id ?? "")}.
|