@bbradar/mcp 0.2.2 → 0.2.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/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { NumericCursorStore } from "./cursorStore.js";
6
6
  import { stableStringify, stripUndefined } from "./json.js";
7
7
  import { FixedWindowRateLimiter } from "./rateLimit.js";
8
8
  import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
9
- const SERVER_VERSION = "0.2.2";
9
+ const SERVER_VERSION = "0.2.3";
10
10
  const MAX_PROGRAMS_PER_SEARCH = 100;
11
11
  const MAX_TARGET_EXPORT = 500;
12
12
  const MAX_RECENT_CHANGES = 100;
@@ -21,6 +21,10 @@ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
21
21
  const MAX_FIND_TARGET_SAMPLES = 20;
22
22
  const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
23
23
  const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
24
+ const DEFAULT_HUNT_PROGRAM_PAGES = 7;
25
+ const MAX_HUNT_RESULTS = 15;
26
+ const MAX_HUNT_TARGETS_PER_RESULT = 10;
27
+ const MAX_BULK_PROGRAM_RESOURCE_LINKS = 5;
24
28
  const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 12;
25
29
  const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
26
30
  const PROGRAM_COLLECTION_CONCURRENCY = 4;
@@ -50,6 +54,7 @@ const CORE_TOOL_NAMES = new Set([
50
54
  "get_program_targets",
51
55
  "get_recent_changes",
52
56
  "get_repository_changes",
57
+ "find_hunt_targets",
53
58
  "find_repository_hunt_targets",
54
59
  "get_latest_added_targets",
55
60
  "get_opportunities",
@@ -397,14 +402,16 @@ export function createBbradarServer(client, config) {
397
402
  "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
398
403
  "This server uses the compact core tool profile. Start with get_bbradar_guide when unsure.",
399
404
  "Use resolve_program or run_program_name_action for a named program.",
400
- "Use find_programs for general discovery and find_programs_by_target for reverse target lookup.",
405
+ "Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
406
+ "Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
401
407
  "Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
402
408
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
403
409
  ]
404
410
  : [
405
411
  "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
406
412
  "When unsure which BBRadar tool to use, call get_bbradar_guide first. It is local-only and cheap.",
407
- "Simple default path: find_program_candidates for discovery, run_program_name_action for one named program, get_program_eligible_targets_by_name for target lists by program name.",
413
+ "Simple default path: find_hunt_targets once for ranked target/program discovery, run_program_name_action for one named program, get_program_eligible_targets_by_name for exhaustive target lists by program name.",
414
+ "Do not repeat program discovery with alternate sorts or fan out get_program_targets across a shortlist; find_hunt_targets already includes exact bounded samples and recent target additions.",
408
415
  "Use advanced tools only after the user's goal is clear or the guide points to them.",
409
416
  "Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
410
417
  "Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
@@ -412,7 +419,7 @@ export function createBbradarServer(client, config) {
412
419
  "Self-Hosted programs use Self-Hosted:slug ids and are available through program search, exact detail, and target tools. Respect data_capabilities; do not infer missing opportunity, report-count, target-change, submission-cost, or repository signals.",
413
420
  "Freshness: find_recently_updated_programs, get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
414
421
  "Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
415
- "Discovery: find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, find_low_noise_programs.",
422
+ "Discovery: find_hunt_targets is the one-call default; find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, and find_low_noise_programs are lower-level alternatives.",
416
423
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
417
424
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
418
425
  ];
@@ -741,6 +748,51 @@ export function createBbradarServer(client, config) {
741
748
  source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
742
749
  }), "repository_changes", "repository changes", continuationStore, config);
743
750
  }));
751
+ registerTool("find_hunt_targets", {
752
+ title: "Find Best Hunt Targets",
753
+ description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, and recently added targets. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
754
+ inputSchema: {
755
+ platforms: platformListSchema(),
756
+ exclude_platforms: stringListSchema("Platforms to exclude from the final shortlist."),
757
+ tags: tagListSchema(),
758
+ scope_tags: scopeTagListSchema(),
759
+ target_types: targetTypeListSchema(),
760
+ language_tags: languageTagListSchema(),
761
+ surface_mode: z.enum(["any", "web2", "web3"]).default("any"),
762
+ min_bounty_max: z.number().int().min(0).optional(),
763
+ require_bounty: z.boolean().default(true),
764
+ max_public_report_count: z.number().int().min(0).optional(),
765
+ include_unknown_report_count: z.boolean().default(true),
766
+ min_eligible_targets: z.number().int().min(0).default(3),
767
+ min_total_targets: z.number().int().min(0).optional(),
768
+ min_wildcards: z.number().int().min(0).max(1000).default(0),
769
+ min_added_7d: z.number().int().min(0).optional(),
770
+ fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
771
+ updated_since: isoDateTimeSchema.optional(),
772
+ min_target_score: z.number().int().min(0).max(100).optional(),
773
+ target_opportunity_label: targetOpportunityLabelSchema.optional(),
774
+ competition_risk: competitionRiskSchema.optional(),
775
+ include_recent_changes: z.boolean().default(true),
776
+ recent_target_days: z.number().int().min(1).max(365).default(30),
777
+ recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
778
+ max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(DEFAULT_HUNT_PROGRAM_PAGES),
779
+ max_results: z.number().int().min(1).max(MAX_HUNT_RESULTS).default(10),
780
+ targets_per_result: z.number().int().min(1).max(MAX_HUNT_TARGETS_PER_RESULT).default(5),
781
+ ranking_mode: z.enum(["balanced", "highest_reward", "widest_scope", "freshest_targets", "lowest_competition"]).default("balanced"),
782
+ upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
783
+ },
784
+ outputSchema: {
785
+ ...apiEnvelopeOutputShape(),
786
+ matches: z.array(jsonRecordSchema()).optional(),
787
+ filters: jsonRecordSchema().optional(),
788
+ errors: z.array(jsonRecordSchema()).optional(),
789
+ upstream_budget: jsonRecordSchema().optional(),
790
+ ranking_scope: jsonRecordSchema().optional(),
791
+ result_guidance: jsonRecordSchema().optional(),
792
+ meta: jsonRecordSchema().optional()
793
+ },
794
+ annotations: readOnlyAnnotations
795
+ }, (args) => runTool("find_hunt_targets", config, rateLimiter, async () => findHuntTargets(client, config, args)));
744
796
  registerTool("find_repository_hunt_targets", {
745
797
  title: "Find Repository-Aware Hunt Targets",
746
798
  description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary platforms, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria.",
@@ -989,7 +1041,7 @@ export function createBbradarServer(client, config) {
989
1041
  }));
990
1042
  registerTool("find_programs", {
991
1043
  title: "Find BBRadar Programs",
992
- description: "Ranked program discovery with positive and negative platform/surface filters and compact output. Strict-filter retries reuse cached upstream pages, so relax one local filter at a time when a narrow query returns no candidates.",
1044
+ description: "Lower-level program-only discovery. For normal 'find good targets/programs' requests, prefer one find_hunt_targets call because it already combines ranking, exact bounded target samples, and recent additions. Use this tool when raw program filtering without target enrichment is specifically needed; keep output_mode=compact and avoid parallel alternate-sort calls.",
993
1045
  inputSchema: {
994
1046
  platforms: platformListSchema(),
995
1047
  exclude_platforms: stringListSchema("Platforms to exclude after retrieval. Use list_filters first when the user asks for a geographic/platform exclusion without naming exact platform values."),
@@ -1881,10 +1933,10 @@ function bbradarGuide(topic, profile, client) {
1881
1933
  }
1882
1934
  const primaryEntryPoints = [
1883
1935
  {
1884
- tool: "find_program_candidates",
1885
- use_when: "The user wants good programs to hunt, broad discovery, or does not name one exact program.",
1936
+ tool: "find_hunt_targets",
1937
+ use_when: "The user wants good targets or programs ranked by rewards, scope, competition, target quality, or recent target additions without requiring repository activity.",
1886
1938
  minimum_arguments: {},
1887
- useful_arguments: ["platforms", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "fresh_launch_days", "has_api_scope"]
1939
+ useful_arguments: ["platforms", "surface_mode", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "recent_target_days", "ranking_mode"]
1888
1940
  },
1889
1941
  {
1890
1942
  tool: "find_repository_hunt_targets",
@@ -1913,13 +1965,13 @@ function bbradarGuide(topic, profile, client) {
1913
1965
  const decisionTree = [
1914
1966
  {
1915
1967
  user_goal: "find programs to hunt",
1916
- first_tool: "find_program_candidates",
1917
- then: "Use get_program_brief on the best program_id if the user wants details."
1968
+ first_tool: "find_hunt_targets",
1969
+ then: "Treat its ranked matches, exact bounded target samples, and recent additions as final unless the user explicitly requests an exhaustive scope dump for one selected program."
1918
1970
  },
1919
1971
  {
1920
1972
  user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
1921
- first_tool: "list_filters",
1922
- then: "Resolve the excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. If strict local filters return zero, relax one threshold and retry; cached source pages make the retry cheap."
1973
+ first_tool: "find_hunt_targets",
1974
+ then: "Call once with surface_mode=web2 and the requested platforms/exclusions. Do not split the platforms or ranking dimensions into separate calls."
1923
1975
  },
1924
1976
  {
1925
1977
  user_goal: "named program summary",
@@ -1991,7 +2043,7 @@ function bbradarGuide(topic, profile, client) {
1991
2043
  }
1992
2044
  ];
1993
2045
  const advancedTools = {
1994
- discovery: ["find_wildcard_programs", "find_low_competition_high_reward", "find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
2046
+ discovery: ["find_hunt_targets", "find_wildcard_programs", "find_low_competition_high_reward", "find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
1995
2047
  freshness: ["find_recently_added_wildcards", "find_recently_updated_programs", "find_fresh_hunt_candidates", "get_latest_added_targets", "get_recent_target_activity", "get_program_scope_delta", "check_watchlist_new_targets"],
1996
2048
  details: ["get_program_brief", "get_program_scope_summary", "get_program_target_breakdown", "find_programs_by_target"],
1997
2049
  exports: ["export_targets"]
@@ -2064,9 +2116,10 @@ function bbradarGuide(topic, profile, client) {
2064
2116
  function coreBbradarGuide(topic, client) {
2065
2117
  const decisionTree = [
2066
2118
  {
2067
- user_goal: "find programs to hunt",
2068
- first_tool: "find_programs",
2069
- arguments: { max_results: 10, output_mode: "compact" }
2119
+ user_goal: "find good hunt targets or programs using rewards, scope, competition, target quality, or recent target additions",
2120
+ first_tool: "find_hunt_targets",
2121
+ arguments: { max_results: 10, ranking_mode: "balanced" },
2122
+ then: "Use this single result as the final shortlist; do not repeat find_programs with alternate sorts or fetch each program's targets."
2070
2123
  },
2071
2124
  {
2072
2125
  user_goal: "named program targets",
@@ -2102,7 +2155,7 @@ function coreBbradarGuide(topic, client) {
2102
2155
  {
2103
2156
  user_goal: "unknown filter values",
2104
2157
  first_tool: "list_filters",
2105
- then: "Reuse observed platforms, tags, target types, and language tags in find_programs."
2158
+ then: "Reuse observed platforms, tags, target types, and language tags in one find_hunt_targets call."
2106
2159
  }
2107
2160
  ];
2108
2161
  return {
@@ -2118,6 +2171,7 @@ function coreBbradarGuide(topic, client) {
2118
2171
  require_bounty: true
2119
2172
  },
2120
2173
  primary_entry_points: [
2174
+ "find_hunt_targets",
2121
2175
  "find_programs",
2122
2176
  "find_repository_hunt_targets",
2123
2177
  "run_program_name_action",
@@ -5633,7 +5687,7 @@ function previewExportText(payload) {
5633
5687
  }
5634
5688
  function collectResourceLinks(payload) {
5635
5689
  const links = new Map();
5636
- for (const program of extractProgramsFromPayload(payload)) {
5690
+ for (const program of extractProgramsFromPayload(payload).slice(0, MAX_BULK_PROGRAM_RESOURCE_LINKS)) {
5637
5691
  const id = stringField(program, "id");
5638
5692
  const resourceLinks = readObject(program.resource_links);
5639
5693
  const programUri = stringField(resourceLinks, "program");
@@ -5981,15 +6035,15 @@ function candidateTradeoffs(candidate) {
5981
6035
  }
5982
6036
  function candidateNextAction(candidate) {
5983
6037
  if (candidate.wildcardCount > 0) {
5984
- return "Review in-scope wildcard targets with get_program_targets and keep follow-up analysis passive and BBRadar-data-backed.";
6038
+ return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive wildcard scope.";
5985
6039
  }
5986
6040
  if (candidateHasApiSurface(candidate)) {
5987
- return "Review API targets with get_program_targets and summarize the allowed API scope from BBRadar data.";
6041
+ return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive API scope.";
5988
6042
  }
5989
6043
  if (candidateHasWebSurface(candidate)) {
5990
- return "Review web/domain targets with get_program_targets and summarize freshness, reward, and report-count signals.";
6044
+ return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive web scope.";
5991
6045
  }
5992
- return "Fetch exact scope with get_program_targets before making recommendations.";
6046
+ return "The program-level ranking is sufficient for a shortlist; fetch exact scope only if the user requests exhaustive details for this program.";
5993
6047
  }
5994
6048
  function targetSurfaceScore(candidate) {
5995
6049
  return candidate.targetSurfaceScore;
@@ -6294,6 +6348,536 @@ function clampLimit(name, value, max, warnings) {
6294
6348
  warnings.push(`${name} was clamped from ${value} to ${max}.`);
6295
6349
  return max;
6296
6350
  }
6351
+ async function findHuntTargets(client, config, input) {
6352
+ const warnings = [];
6353
+ const errors = [];
6354
+ const recentChangesEnabled = input.include_recent_changes && client.supportsEndpoint("target_changes");
6355
+ const desiredRecentChangeQueries = recentChangesEnabled ? huntRecentChangeQueries(input) : [];
6356
+ const recentChangeQueries = desiredRecentChangeQueries.slice(0, Math.max(0, input.upstream_request_budget - 2));
6357
+ const recentRequestReserve = recentChangeQueries.length;
6358
+ const candidatePoolSize = Math.min(MAX_FIND_PROGRAM_RESULTS, Math.max(input.max_results, input.max_results * 3));
6359
+ const programRequestBudget = Math.min(input.max_program_pages, Math.max(1, Math.ceil((input.upstream_request_budget - recentRequestReserve) / 2)));
6360
+ const programInput = {
6361
+ platforms: input.platforms,
6362
+ exclude_platforms: input.exclude_platforms,
6363
+ tags: input.tags,
6364
+ scope_tags: input.scope_tags,
6365
+ target_types: input.target_types,
6366
+ language_tags: input.language_tags,
6367
+ web3: input.surface_mode === "web3",
6368
+ exclude_web3: input.surface_mode === "web2",
6369
+ opportunity_levels: [],
6370
+ updated_since: input.updated_since,
6371
+ min_bounty_min: undefined,
6372
+ min_bounty_max: input.min_bounty_max,
6373
+ max_bounty_max: undefined,
6374
+ require_bounty: input.require_bounty,
6375
+ max_public_report_count: input.max_public_report_count,
6376
+ require_known_report_count: false,
6377
+ include_unknown_report_count: input.include_unknown_report_count,
6378
+ exclude_ctf: true,
6379
+ exclude_mobile_only: input.surface_mode === "web2",
6380
+ exclude_non_web: input.surface_mode === "web2",
6381
+ has_api_scope: false,
6382
+ contest_like: false,
6383
+ vdp_mode: "exclude",
6384
+ fresh_launch_days: input.fresh_launch_days,
6385
+ min_wildcards: input.min_wildcards,
6386
+ min_eligible_targets: input.min_eligible_targets,
6387
+ min_total_targets: input.min_total_targets,
6388
+ min_added_24h: undefined,
6389
+ min_added_7d: input.min_added_7d,
6390
+ max_pages: input.max_program_pages,
6391
+ max_results: candidatePoolSize,
6392
+ sort_by: huntProgramSort(input.ranking_mode),
6393
+ include_target_samples: false,
6394
+ target_sample_programs: 0,
6395
+ target_sample_size: 1,
6396
+ only_in_scope_targets: true,
6397
+ only_bounty_eligible_targets: true,
6398
+ target_samples_only_wildcards: false,
6399
+ output_mode: "compact",
6400
+ upstream_request_budget: programRequestBudget
6401
+ };
6402
+ if (input.include_recent_changes && !recentChangesEnabled) {
6403
+ warnings.push("The negotiated API contract does not expose target changes; ranked current programs without recent-change enrichment.");
6404
+ }
6405
+ else if (recentChangeQueries.length < desiredRecentChangeQueries.length) {
6406
+ warnings.push(`Recent target-change queries were reduced from ${desiredRecentChangeQueries.length} to ${recentChangeQueries.length} to preserve the upstream request budget.`);
6407
+ }
6408
+ const [programPayload, recentResult] = await Promise.all([
6409
+ findPrograms(client, config, programInput),
6410
+ recentChangesEnabled
6411
+ ? fetchHuntRecentChanges(client, config, input, recentChangeQueries)
6412
+ : Promise.resolve({ changes: [], sourceRequests: [], requestsUsed: 0, errors: [] })
6413
+ ]);
6414
+ const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
6415
+ warnings.push(...programWarnings);
6416
+ errors.push(...recentResult.errors);
6417
+ const programBudget = readObject(programPayload.upstream_budget);
6418
+ const programRequestsUsed = readNumber(programBudget?.used) ?? 0;
6419
+ const targetRequestBudget = Math.max(0, input.upstream_request_budget - programRequestsUsed - recentResult.requestsUsed);
6420
+ const programEntries = readArray(programPayload.programs)
6421
+ .map(readObject)
6422
+ .filter((program) => program !== undefined);
6423
+ const recentByProgram = groupRecentHuntChanges(recentResult.changes, input);
6424
+ const preliminaryRankedEntries = programEntries
6425
+ .map((entry) => ({
6426
+ entry,
6427
+ ranking: buildHuntTargetMatch(entry, new Map(), recentByProgram, input)
6428
+ }))
6429
+ .sort((left, right) => compareHuntTargetMatches(left.ranking, right.ranking, input.ranking_mode));
6430
+ const sampledProgramCount = Math.min(input.max_results, preliminaryRankedEntries.length, targetRequestBudget);
6431
+ const finalistEntries = preliminaryRankedEntries
6432
+ .slice(0, sampledProgramCount)
6433
+ .map((entry) => entry.entry);
6434
+ const targetResult = await collectHuntTargetSamples(client, finalistEntries, input);
6435
+ errors.push(...targetResult.errors);
6436
+ if (sampledProgramCount < Math.min(input.max_results, programEntries.length)) {
6437
+ warnings.push(`The upstream request budget limited the final exact target shortlist to ${sampledProgramCount} of ${Math.min(input.max_results, programEntries.length)} requested results.`);
6438
+ }
6439
+ const ranked = finalistEntries
6440
+ .map((entry) => buildHuntTargetMatch(entry, targetResult.targetsByProgram, recentByProgram, input))
6441
+ .sort((left, right) => compareHuntTargetMatches(left, right, input.ranking_mode))
6442
+ .slice(0, input.max_results);
6443
+ const matches = ranked.map((entry, index) => ({
6444
+ rank: index + 1,
6445
+ ...entry.payload
6446
+ }));
6447
+ const targetRequestsUsed = targetResult.requestsUsed;
6448
+ const totalRequestsUsed = programRequestsUsed + recentResult.requestsUsed + targetRequestsUsed;
6449
+ return stripUndefined({
6450
+ request_id: randomUUID(),
6451
+ fetched_at: new Date().toISOString(),
6452
+ source_requests: [
6453
+ ...readArray(programPayload.source_requests),
6454
+ ...recentResult.sourceRequests,
6455
+ ...targetResult.sourceRequests
6456
+ ],
6457
+ warnings: uniqueStrings(warnings).length > 0 ? uniqueStrings(warnings) : undefined,
6458
+ errors: errors.length > 0 ? errors : undefined,
6459
+ filters: stripUndefined({
6460
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
6461
+ exclude_platforms: input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
6462
+ tags: input.tags.length > 0 ? input.tags : undefined,
6463
+ scope_tags: input.scope_tags.length > 0 ? input.scope_tags : undefined,
6464
+ target_types: input.target_types.length > 0 ? input.target_types : undefined,
6465
+ language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
6466
+ surface_mode: input.surface_mode,
6467
+ min_bounty_max: input.min_bounty_max,
6468
+ require_bounty: input.require_bounty,
6469
+ max_public_report_count: input.max_public_report_count,
6470
+ min_eligible_targets: input.min_eligible_targets,
6471
+ min_total_targets: input.min_total_targets,
6472
+ min_wildcards: input.min_wildcards,
6473
+ min_added_7d: input.min_added_7d,
6474
+ fresh_launch_days: input.fresh_launch_days,
6475
+ updated_since: input.updated_since,
6476
+ min_target_score: input.min_target_score,
6477
+ target_opportunity_label: input.target_opportunity_label,
6478
+ competition_risk: input.competition_risk,
6479
+ recent_target_days: input.include_recent_changes ? input.recent_target_days : undefined,
6480
+ ranking_mode: input.ranking_mode
6481
+ }),
6482
+ upstream_budget: {
6483
+ initial: input.upstream_request_budget,
6484
+ used: totalRequestsUsed,
6485
+ remaining: Math.max(0, input.upstream_request_budget - totalRequestsUsed)
6486
+ },
6487
+ ranking_scope: stripUndefined({
6488
+ ...(readObject(programPayload.ranking_scope) ?? {}),
6489
+ single_program_catalog_pass: true,
6490
+ recent_change_window_days: input.include_recent_changes ? input.recent_target_days : undefined,
6491
+ recent_change_page_size: input.include_recent_changes ? input.recent_changes_page_size : undefined
6492
+ }),
6493
+ result_guidance: {
6494
+ final_shortlist: true,
6495
+ follow_up_calls_required: false,
6496
+ instruction: "Use these matches as the final shortlist. Exact bounded target samples and recent additions are already included; call get_program_targets only if the user explicitly requests an exhaustive scope dump for one selected program."
6497
+ },
6498
+ matches,
6499
+ meta: {
6500
+ returned: matches.length,
6501
+ program_candidates: programEntries.length,
6502
+ exact_target_programs_sampled: targetResult.targetsByProgram.size,
6503
+ recent_changes_scanned: recentResult.changes.length,
6504
+ recent_programs_matched: recentByProgram.size,
6505
+ model_round_trips_expected: 1,
6506
+ parallel_recent_change_fetch: recentChangeQueries.length > 0,
6507
+ bulk_resource_links_suppressed: true,
6508
+ partial_success: errors.length > 0
6509
+ }
6510
+ });
6511
+ }
6512
+ function huntProgramSort(mode) {
6513
+ if (mode === "highest_reward") {
6514
+ return "highest_reward";
6515
+ }
6516
+ if (mode === "widest_scope") {
6517
+ return "most_eligible_targets";
6518
+ }
6519
+ if (mode === "freshest_targets") {
6520
+ return "most_new_targets";
6521
+ }
6522
+ if (mode === "lowest_competition") {
6523
+ return "lowest_reports";
6524
+ }
6525
+ return "best_hunt_value";
6526
+ }
6527
+ async function fetchHuntRecentChanges(client, config, input, targetTypes) {
6528
+ const results = await Promise.all(targetTypes.map(async (targetType) => {
6529
+ try {
6530
+ const api = await client.getRecentChanges(toQuery({
6531
+ change_type: "added",
6532
+ minTargetScore: input.min_target_score,
6533
+ targetOpportunityLabel: input.target_opportunity_label,
6534
+ competitionRisk: input.competition_risk,
6535
+ include_removed: false,
6536
+ include_ineligible: false,
6537
+ include_out_of_scope: false,
6538
+ platforms: input.platforms,
6539
+ tags: targetType ? [targetType] : undefined,
6540
+ page: 1,
6541
+ page_size: input.recent_changes_page_size
6542
+ }));
6543
+ const data = readObject(api.data);
6544
+ return {
6545
+ changes: readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl)),
6546
+ sourceRequest: {
6547
+ source: "recent_changes",
6548
+ target_type: targetType,
6549
+ request_id: api.requestId,
6550
+ upstream_request_id: api.upstreamRequestId,
6551
+ ...apiSourceMetadata(api),
6552
+ page: 1
6553
+ },
6554
+ requestsUsed: api.cached || api.coalesced ? 0 : 1,
6555
+ error: undefined
6556
+ };
6557
+ }
6558
+ catch (error) {
6559
+ return {
6560
+ changes: [],
6561
+ sourceRequest: undefined,
6562
+ requestsUsed: error instanceof BBRadarApiError && (error.cached || error.coalesced) ? 0 : 1,
6563
+ error: apiOperationError(targetType ? `recent ${targetType} target changes` : "recent target changes", error)
6564
+ };
6565
+ }
6566
+ }));
6567
+ const changes = new Map();
6568
+ for (const result of results) {
6569
+ for (const change of result.changes) {
6570
+ const id = readNumber(change.id) ?? stringField(change, "id");
6571
+ const target = readObject(change.target);
6572
+ const program = readObject(change.program);
6573
+ const fallbackKey = `${stringField(program, "id") ?? ""}:${targetIdentifier(target ?? {}) ?? ""}:${stringField(change, "changed_at") ?? ""}`;
6574
+ changes.set(id === undefined ? fallbackKey : String(id), change);
6575
+ }
6576
+ }
6577
+ return {
6578
+ changes: [...changes.values()],
6579
+ sourceRequests: results
6580
+ .map((result) => result.sourceRequest)
6581
+ .filter((request) => request !== undefined),
6582
+ requestsUsed: results.reduce((total, result) => total + result.requestsUsed, 0),
6583
+ errors: results
6584
+ .map((result) => result.error)
6585
+ .filter((error) => error !== undefined)
6586
+ };
6587
+ }
6588
+ function huntRecentChangeQueries(input) {
6589
+ const requested = uniqueStrings(input.target_types);
6590
+ if (requested.length > 0 && requested.length <= 3) {
6591
+ return requested;
6592
+ }
6593
+ if (requested.length > 3) {
6594
+ return [undefined];
6595
+ }
6596
+ if (input.surface_mode === "web2") {
6597
+ return ["domain", "api", "wildcard"];
6598
+ }
6599
+ if (input.surface_mode === "web3") {
6600
+ return ["source code", "smart contract", "blockchain-dlt"];
6601
+ }
6602
+ return [undefined];
6603
+ }
6604
+ async function collectHuntTargetSamples(client, programs, input) {
6605
+ const targetsByProgram = new Map();
6606
+ const sourceRequestsByIndex = new Array(programs.length);
6607
+ const errorsByIndex = new Array(programs.length);
6608
+ let requestsUsed = 0;
6609
+ await mapWithConcurrency(programs.map((program, index) => ({ program, index })), TARGET_SAMPLE_CONCURRENCY, async ({ program, index }) => {
6610
+ const programId = stringField(program, "id");
6611
+ if (!programId) {
6612
+ return;
6613
+ }
6614
+ try {
6615
+ const requestedPageSize = Math.max(input.targets_per_result, input.target_types.length > 1 ? input.targets_per_result * 10 : input.targets_per_result * 4, 20);
6616
+ const api = await client.getProgramTargets(programId, {
6617
+ target_type: input.target_types.length === 1 ? input.target_types[0] : undefined,
6618
+ eligibility: "eligible",
6619
+ minTargetScore: input.min_target_score,
6620
+ targetOpportunityLabel: input.target_opportunity_label,
6621
+ competitionRisk: input.competition_risk,
6622
+ page: 1,
6623
+ page_size: Math.min(client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM), requestedPageSize)
6624
+ });
6625
+ const data = readObject(api.data);
6626
+ const matchingTargets = readArray(data?.targets)
6627
+ .map(sanitizeTarget)
6628
+ .filter((target) => huntTargetMatches(target, input));
6629
+ const targets = selectHuntTargetSamples(matchingTargets, input.targets_per_result, input.recent_target_days);
6630
+ targetsByProgram.set(programId, targets);
6631
+ sourceRequestsByIndex[index] = {
6632
+ source: "program_targets",
6633
+ program_id: programId,
6634
+ request_id: api.requestId,
6635
+ upstream_request_id: api.upstreamRequestId,
6636
+ ...apiSourceMetadata(api)
6637
+ };
6638
+ if (!api.cached && !api.coalesced) {
6639
+ requestsUsed += 1;
6640
+ }
6641
+ }
6642
+ catch (error) {
6643
+ errorsByIndex[index] = apiOperationError(programId, error);
6644
+ if (!(error instanceof BBRadarApiError) || (!error.cached && !error.coalesced)) {
6645
+ requestsUsed += 1;
6646
+ }
6647
+ }
6648
+ });
6649
+ return {
6650
+ targetsByProgram,
6651
+ sourceRequests: sourceRequestsByIndex.filter((request) => request !== undefined),
6652
+ requestsUsed,
6653
+ errors: errorsByIndex.filter((error) => error !== undefined)
6654
+ };
6655
+ }
6656
+ function huntTargetMatches(target, input) {
6657
+ if (target.in_scope !== true || target.eligible_for_bounty !== true) {
6658
+ return false;
6659
+ }
6660
+ if (input.target_types.length > 0 && !input.target_types.some((targetType) => targetMatchesRequestedType(target, targetType))) {
6661
+ return false;
6662
+ }
6663
+ if (input.scope_tags.length > 0 && !arraysIntersectNormalized(new Set(input.scope_tags.map(normalizeTag)), readStringArrayField(target, "scope_tags"))) {
6664
+ return false;
6665
+ }
6666
+ if (input.language_tags.length > 0 && !arraysIntersectNormalized(new Set(input.language_tags.map(normalizeTag)), readStringArrayField(target, "language_tags"))) {
6667
+ return false;
6668
+ }
6669
+ if (input.min_target_score !== undefined && (readNumber(target.target_opportunity_score) ?? -1) < input.min_target_score) {
6670
+ return false;
6671
+ }
6672
+ if (input.target_opportunity_label !== undefined && stringField(target, "target_opportunity_label") !== input.target_opportunity_label) {
6673
+ return false;
6674
+ }
6675
+ if (input.competition_risk !== undefined && stringField(target, "competition_risk") !== input.competition_risk) {
6676
+ return false;
6677
+ }
6678
+ return true;
6679
+ }
6680
+ function compareHuntTargets(left, right) {
6681
+ return (compareNumbersDesc(readNumber(left.target_opportunity_score), readNumber(right.target_opportunity_score)) ||
6682
+ Number(readBoolean(right.wildcard) === true) - Number(readBoolean(left.wildcard) === true) ||
6683
+ (targetIdentifier(left) ?? "").localeCompare(targetIdentifier(right) ?? ""));
6684
+ }
6685
+ function selectHuntTargetSamples(targets, limit, recentTargetDays) {
6686
+ const selected = new Map();
6687
+ const cutoff = Date.now() - recentTargetDays * 24 * 60 * 60 * 1000;
6688
+ const newest = [...targets]
6689
+ .filter((target) => huntTargetAddedTimestamp(target) >= cutoff)
6690
+ .sort((left, right) => huntTargetAddedTimestamp(right) - huntTargetAddedTimestamp(left));
6691
+ const byQuality = [...targets].sort(compareHuntTargets);
6692
+ for (const target of newest.slice(0, Math.min(2, limit))) {
6693
+ const identifier = targetIdentifier(target);
6694
+ if (identifier) {
6695
+ selected.set(identifier, target);
6696
+ }
6697
+ }
6698
+ for (const target of byQuality) {
6699
+ if (selected.size >= limit) {
6700
+ break;
6701
+ }
6702
+ const identifier = targetIdentifier(target);
6703
+ if (identifier && !selected.has(identifier)) {
6704
+ selected.set(identifier, target);
6705
+ }
6706
+ }
6707
+ return [...selected.values()];
6708
+ }
6709
+ function huntTargetAddedTimestamp(target) {
6710
+ const addedAt = stringField(target, "added_at") ?? stringField(target, "source_added_at");
6711
+ const timestamp = addedAt ? Date.parse(addedAt) : Number.NaN;
6712
+ return Number.isFinite(timestamp) ? timestamp : 0;
6713
+ }
6714
+ function groupRecentHuntChanges(changes, input) {
6715
+ const cutoff = Date.now() - input.recent_target_days * 24 * 60 * 60 * 1000;
6716
+ const byProgram = new Map();
6717
+ for (const change of changes) {
6718
+ if (stringField(change, "change_type") !== "added" || timestampField(change, "changed_at") < cutoff) {
6719
+ continue;
6720
+ }
6721
+ const program = readObject(change.program);
6722
+ const target = readObject(change.target);
6723
+ const programId = stringField(program, "id");
6724
+ if (!programId || !target || !huntTargetMatches(target, input)) {
6725
+ continue;
6726
+ }
6727
+ const existing = byProgram.get(programId) ?? [];
6728
+ const identifier = targetIdentifier(target);
6729
+ if (!identifier || !existing.some((entry) => targetIdentifier(readObject(entry.target) ?? {}) === identifier)) {
6730
+ existing.push(change);
6731
+ existing.sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
6732
+ byProgram.set(programId, existing);
6733
+ }
6734
+ }
6735
+ return byProgram;
6736
+ }
6737
+ function buildHuntTargetMatch(entry, targetsByProgram, recentByProgram, input) {
6738
+ const programId = stringField(entry, "id") ?? "";
6739
+ const targetSamples = targetsByProgram.get(programId) ?? [];
6740
+ const recentChanges = recentByProgram.get(programId) ?? [];
6741
+ const recentTargets = recentHuntTargetEntries(targetSamples, recentChanges, input.recent_target_days);
6742
+ const rankFactors = readObject(entry.rank_factors) ?? {};
6743
+ const latestRecentAt = recentTargets[0]?.addedAt;
6744
+ const rewardScore = normalizedRewardScore(readNumber(rankFactors.bounty_max));
6745
+ const scopeScore = Math.min(100, (readNumber(rankFactors.eligible_targets) ?? 0) * 2 +
6746
+ (readNumber(rankFactors.wildcard_targets) ?? 0) * 5 +
6747
+ (readNumber(rankFactors.target_surface_score) ?? 0) * 0.25);
6748
+ const freshnessScore = huntFreshnessScore(latestRecentAt, recentTargets.length, readNumber(rankFactors.added_7d));
6749
+ const publicReports = readNumber(rankFactors.public_report_count);
6750
+ const competitionScore = publicReports === undefined ? 50 : Math.max(0, 100 - Math.min(100, publicReports / 10));
6751
+ const bestTargetScore = Math.max(0, ...targetSamples.map((target) => readNumber(target.target_opportunity_score) ?? 0));
6752
+ const baseHuntScore = readNumber(rankFactors.hunt_value_score) ?? 0;
6753
+ const compositeScore = huntCompositeScore(input.ranking_mode, baseHuntScore, rewardScore, scopeScore, freshnessScore, competitionScore, bestTargetScore);
6754
+ const program = compactProgram(entry);
6755
+ delete program.resource_links;
6756
+ const existingReasons = readArray(entry.why_ranked_here).filter((reason) => typeof reason === "string");
6757
+ const reasons = [...existingReasons];
6758
+ if (recentTargets.length > 0) {
6759
+ reasons.push(`${recentTargets.length} eligible target additions in the last ${input.recent_target_days} days`);
6760
+ }
6761
+ if (bestTargetScore > 0) {
6762
+ reasons.push(`best sampled target score ${bestTargetScore}`);
6763
+ }
6764
+ return {
6765
+ compositeScore,
6766
+ rewardScore,
6767
+ scopeScore,
6768
+ freshnessScore,
6769
+ competitionScore,
6770
+ payload: stripUndefined({
6771
+ program,
6772
+ match_score: compositeScore,
6773
+ rank_factors: {
6774
+ ...rankFactors,
6775
+ reward_score: roundScore(rewardScore),
6776
+ scope_score: roundScore(scopeScore),
6777
+ freshness_score: roundScore(freshnessScore),
6778
+ competition_score: roundScore(competitionScore),
6779
+ best_sample_target_score: bestTargetScore > 0 ? bestTargetScore : undefined,
6780
+ recent_added_targets_in_window: recentTargets.length,
6781
+ latest_added_target_at: latestRecentAt,
6782
+ composite_score: compositeScore
6783
+ },
6784
+ why_ranked_here: uniqueStrings(reasons),
6785
+ tradeoffs: readArray(entry.tradeoffs).filter((tradeoff) => typeof tradeoff === "string"),
6786
+ target_samples: targetSamples.map(compactHuntTarget),
6787
+ recent_added_targets: recentTargets.slice(0, input.targets_per_result).map((recent) => ({
6788
+ ...compactHuntTarget(recent.target),
6789
+ added_at: recent.addedAt,
6790
+ freshness_source: recent.source
6791
+ }))
6792
+ })
6793
+ };
6794
+ }
6795
+ function compactHuntTarget(target) {
6796
+ return stripUndefined({
6797
+ ...compactTarget(target),
6798
+ added_at: stringField(target, "added_at"),
6799
+ source_added_at: stringField(target, "source_added_at"),
6800
+ last_changed_at: stringField(target, "last_changed_at")
6801
+ });
6802
+ }
6803
+ function recentHuntTargetEntries(targetSamples, recentChanges, recentTargetDays) {
6804
+ const cutoff = Date.now() - recentTargetDays * 24 * 60 * 60 * 1000;
6805
+ const entries = new Map();
6806
+ const remember = (target, addedAt, source) => {
6807
+ const identifier = targetIdentifier(target);
6808
+ const timestamp = addedAt ? Date.parse(addedAt) : Number.NaN;
6809
+ if (!identifier || !addedAt || !Number.isFinite(timestamp) || timestamp < cutoff) {
6810
+ return;
6811
+ }
6812
+ const existing = entries.get(identifier);
6813
+ if (!existing || timestamp > existing.timestamp || (timestamp === existing.timestamp && source === "target_change")) {
6814
+ entries.set(identifier, { target, addedAt, source, timestamp });
6815
+ }
6816
+ };
6817
+ for (const change of recentChanges) {
6818
+ const target = readObject(change.target);
6819
+ if (target) {
6820
+ remember(target, stringField(change, "changed_at"), "target_change");
6821
+ }
6822
+ }
6823
+ for (const target of targetSamples) {
6824
+ remember(target, stringField(target, "added_at") ?? stringField(target, "source_added_at"), "target_inventory");
6825
+ }
6826
+ return [...entries.values()].sort((left, right) => right.timestamp - left.timestamp);
6827
+ }
6828
+ function normalizedRewardScore(reward) {
6829
+ if (reward === undefined || reward <= 0) {
6830
+ return 0;
6831
+ }
6832
+ return Math.min(100, Math.log10(reward + 1) / Math.log10(100_001) * 100);
6833
+ }
6834
+ function huntFreshnessScore(latestAt, recentCount, added7d) {
6835
+ if (latestAt) {
6836
+ const timestamp = Date.parse(latestAt);
6837
+ if (Number.isFinite(timestamp)) {
6838
+ const ageDays = Math.max(0, (Date.now() - timestamp) / (24 * 60 * 60 * 1000));
6839
+ return Math.min(100, Math.max(0, 100 - ageDays * 2) + Math.min(20, recentCount * 4));
6840
+ }
6841
+ }
6842
+ return Math.min(100, Math.max(0, added7d ?? 0) * 12);
6843
+ }
6844
+ function huntCompositeScore(mode, baseHuntScore, rewardScore, scopeScore, freshnessScore, competitionScore, bestTargetScore) {
6845
+ if (mode === "highest_reward") {
6846
+ return roundScore(rewardScore * 0.7 + baseHuntScore * 0.2 + scopeScore * 0.1);
6847
+ }
6848
+ if (mode === "widest_scope") {
6849
+ return roundScore(scopeScore * 0.7 + baseHuntScore * 0.2 + rewardScore * 0.1);
6850
+ }
6851
+ if (mode === "freshest_targets") {
6852
+ return roundScore(freshnessScore * 0.7 + bestTargetScore * 0.15 + baseHuntScore * 0.15);
6853
+ }
6854
+ if (mode === "lowest_competition") {
6855
+ return roundScore(competitionScore * 0.65 + baseHuntScore * 0.2 + rewardScore * 0.15);
6856
+ }
6857
+ return roundScore(baseHuntScore * 0.35 +
6858
+ rewardScore * 0.2 +
6859
+ scopeScore * 0.2 +
6860
+ freshnessScore * 0.15 +
6861
+ bestTargetScore * 0.1);
6862
+ }
6863
+ function roundScore(value) {
6864
+ return Math.round(value * 100) / 100;
6865
+ }
6866
+ function compareHuntTargetMatches(left, right, mode) {
6867
+ if (mode === "highest_reward") {
6868
+ return right.rewardScore - left.rewardScore || right.compositeScore - left.compositeScore;
6869
+ }
6870
+ if (mode === "widest_scope") {
6871
+ return right.scopeScore - left.scopeScore || right.compositeScore - left.compositeScore;
6872
+ }
6873
+ if (mode === "freshest_targets") {
6874
+ return right.freshnessScore - left.freshnessScore || right.compositeScore - left.compositeScore;
6875
+ }
6876
+ if (mode === "lowest_competition") {
6877
+ return right.competitionScore - left.competitionScore || right.compositeScore - left.compositeScore;
6878
+ }
6879
+ return right.compositeScore - left.compositeScore;
6880
+ }
6297
6881
  async function findRepositoryHuntTargets(client, config, input) {
6298
6882
  const warnings = [];
6299
6883
  const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
@@ -7490,9 +8074,11 @@ function readStringArrayField(value, field) {
7490
8074
  }
7491
8075
  function registerPrompts(server, profile) {
7492
8076
  const discoveryToolInstructions = profile === "core"
7493
- ? `- Start with find_programs and pass opportunity_levels from the requested value below unless the user asked for a broader search.
7494
- - Express language, target type, reward, Web3, wildcard, VDP, and platform needs through find_programs filters.`
7495
- : `- Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from the requested value below.
8077
+ ? `- Start with find_hunt_targets for ranked target/program discovery. Make one call and use its shortlist as final; do not repeat find_programs with alternate sorts or fetch each candidate's targets.
8078
+ - Express language, target type, reward, Web2/Web3, wildcard, competition, freshness, and platform needs through find_hunt_targets filters.
8079
+ - Use find_repository_hunt_targets instead when the user explicitly requires Git or repository activity.`
8080
+ : `- Start with find_hunt_targets for ranked target/program discovery; use its included target samples and recent additions as the final shortlist.
8081
+ - Use find_program_candidates only for lower-level program-only discovery; pass opportunity_levels from the requested value below.
7496
8082
  - Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs only for explicit VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.`;
7497
8083
  server.registerPrompt("find_best_programs_to_hunt", {
7498
8084
  title: "Find Best Programs To Hunt",
@@ -7507,7 +8093,7 @@ function registerPrompts(server, profile) {
7507
8093
 
7508
8094
  Find the best BBRadar program candidates.
7509
8095
  ${discoveryToolInstructions}
7510
- - Use this opportunity level JSON array: ${promptJson([args.opportunity_level ?? "elite"])}.
8096
+ - Treat this opportunity level JSON array as a ranking preference, not a strict first-pass filter: ${promptJson([args.opportunity_level ?? "elite"])}.
7511
8097
  - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
7512
8098
  - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
7513
8099
  - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.