@bbradar/mcp 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,11 +1,12 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { z } from "zod";
4
4
  import { BBRadarApiError } from "./bbradarClient.js";
5
+ import { NumericCursorStore } from "./cursorStore.js";
5
6
  import { stableStringify, stripUndefined } from "./json.js";
6
7
  import { FixedWindowRateLimiter } from "./rateLimit.js";
7
- import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
8
- const SERVER_VERSION = "0.1.0";
8
+ import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
9
+ const SERVER_VERSION = "0.2.0";
9
10
  const MAX_PROGRAMS_PER_SEARCH = 100;
10
11
  const MAX_TARGET_EXPORT = 500;
11
12
  const MAX_RECENT_CHANGES = 100;
@@ -20,7 +21,7 @@ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
20
21
  const MAX_FIND_TARGET_SAMPLES = 20;
21
22
  const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
22
23
  const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
23
- const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 4;
24
+ const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 12;
24
25
  const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
25
26
  const PROGRAM_COLLECTION_CONCURRENCY = 4;
26
27
  const TARGET_SAMPLE_CONCURRENCY = 3;
@@ -29,13 +30,40 @@ const MAX_FILTER_DISCOVERY_PAGES = 5;
29
30
  const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
30
31
  const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
31
32
  const MAX_LOCAL_EXPORT_RESOURCES = 25;
33
+ const MAX_LOCAL_EXPORT_BYTES = 64 * 1024 * 1024;
34
+ const LOCAL_EXPORT_TTL_MS = 10 * 60 * 1000;
32
35
  const EXPORT_PREVIEW_LIMIT = 25;
33
36
  const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
34
- const STALE_PROGRAM_ID_RESOLVE_BUDGET = 8;
37
+ const STALE_PROGRAM_ID_RESOLVE_BUDGET = 12;
35
38
  const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
39
+ const MAX_IN_MEMORY_FEED_CURSORS = 500;
36
40
  const MAX_RESOLVE_SEARCH_QUERIES = 4;
41
+ const FAST_RESOLVE_SEARCH_QUERIES = 2;
37
42
  const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
38
43
  const SDK_VERSION = "1.29.0";
44
+ const CORE_TOOL_NAMES = new Set([
45
+ "search_programs",
46
+ "get_program",
47
+ "resolve_program",
48
+ "run_program_name_action",
49
+ "get_program_eligible_targets_by_name",
50
+ "get_program_targets",
51
+ "get_recent_changes",
52
+ "get_repository_changes",
53
+ "get_latest_added_targets",
54
+ "get_opportunities",
55
+ "find_programs",
56
+ "find_programs_by_target",
57
+ "list_filters",
58
+ "get_bbradar_guide",
59
+ "get_mcp_status",
60
+ "export_targets"
61
+ ]);
62
+ const TOOL_ENDPOINT_REQUIREMENTS = {
63
+ get_repository_changes: "repository_changes",
64
+ export_targets: "target_export",
65
+ get_opportunities: "opportunities"
66
+ };
39
67
  const PROGRAM_ID_PLATFORM_GUESSES = [
40
68
  "HackerOne",
41
69
  "BugCrowd",
@@ -141,6 +169,7 @@ const TARGET_QUERY_STOP_WORDS = new Set([
141
169
  ]);
142
170
  const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
143
171
  const changeListModeSchema = z.enum(["compact", "full"]);
172
+ const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
144
173
  const programListModeSchema = z.enum(["compact", "full"]);
145
174
  const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
146
175
  const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
@@ -209,6 +238,8 @@ const pageSchema = z.number().int().min(1).default(1);
209
238
  const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
210
239
  const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
211
240
  const opportunityLevelSchema = z.enum(["elite", "hot", "strong", "potential"]);
241
+ const targetOpportunityLabelSchema = z.enum(["Elite Target", "Strong Target", "Potential Target", "Normal"]);
242
+ const competitionRiskSchema = z.enum(["Low", "Medium", "High", "Unknown"]);
212
243
  const programSortSchema = z
213
244
  .enum([
214
245
  "best_match",
@@ -260,6 +291,7 @@ function toolTimingOutputSchema() {
260
291
  function cacheOutputSchema() {
261
292
  return z.object({
262
293
  hit: z.boolean(),
294
+ stale_while_revalidate: z.boolean().optional(),
263
295
  coalesced_live_request: z.boolean().optional(),
264
296
  expires_at: z.string().optional()
265
297
  });
@@ -346,12 +378,28 @@ const programCandidatePresetInputSchema = {
346
378
  output_mode: programListModeSchema.default("compact"),
347
379
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
348
380
  };
381
+ function createProfiledToolRegistrar(server, profile, client) {
382
+ return (name, toolConfig, callback) => {
383
+ const registeredTool = server.registerTool(name, toolConfig, callback);
384
+ const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
385
+ if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
386
+ (requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
387
+ registeredTool.disable();
388
+ }
389
+ return registeredTool;
390
+ };
391
+ }
349
392
  export function createBbradarServer(client, config) {
350
- const server = new McpServer({
351
- name: "bbradar.io",
352
- version: SERVER_VERSION
353
- }, {
354
- instructions: [
393
+ const instructions = config.toolProfile === "core"
394
+ ? [
395
+ "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
396
+ "This server uses the compact core tool profile. Start with get_bbradar_guide when unsure.",
397
+ "Use resolve_program or run_program_name_action for a named program.",
398
+ "Use find_programs for general discovery and find_programs_by_target for reverse target lookup.",
399
+ "Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
400
+ "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
401
+ ]
402
+ : [
355
403
  "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
356
404
  "When unsure which BBRadar tool to use, call get_bbradar_guide first. It is local-only and cheap.",
357
405
  "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.",
@@ -359,18 +407,39 @@ export function createBbradarServer(client, config) {
359
407
  "Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
360
408
  "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.",
361
409
  "Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
410
+ "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.",
362
411
  "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.",
363
- "Discovery: find_program_candidates, 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.",
412
+ "Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
413
+ "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.",
364
414
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
365
415
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
366
- ].join("\n")
416
+ ];
417
+ const server = new McpServer({
418
+ name: "bbradar.io",
419
+ version: SERVER_VERSION
420
+ }, {
421
+ instructions: instructions.join("\n")
367
422
  });
423
+ const registerTool = createProfiledToolRegistrar(server, config.toolProfile, client);
368
424
  const rateLimiter = new FixedWindowRateLimiter();
369
425
  const exportStore = new Map();
370
- server.registerTool("search_programs", {
426
+ const continuationStore = new Map();
427
+ const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
428
+ const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
429
+ const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
430
+ const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
431
+ const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
432
+ const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
433
+ const maxTargetExportProgramIds = client.endpointNumber("target_export", "max_program_ids", 50);
434
+ const programsPageSizeSchema = z.number().int().min(1).max(maxProgramsPageSize).default(Math.min(20, maxProgramsPageSize));
435
+ const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
436
+ const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
437
+ const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
438
+ registerTool("search_programs", {
371
439
  title: "Search BBRadar Programs",
372
- description: `List active programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
440
+ description: `Search or list active platform and Self-Hosted programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
373
441
  inputSchema: {
442
+ search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
374
443
  platforms: platformListSchema(),
375
444
  tags: tagListSchema(),
376
445
  updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
@@ -393,7 +462,7 @@ export function createBbradarServer(client, config) {
393
462
  meta: sanitizeJson(data?.meta)
394
463
  });
395
464
  }));
396
- server.registerTool("get_program", {
465
+ registerTool("get_program", {
397
466
  title: "Get BBRadar Program",
398
467
  description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
399
468
  inputSchema: {
@@ -429,7 +498,7 @@ export function createBbradarServer(client, config) {
429
498
  }
430
499
  }));
431
500
  }));
432
- server.registerTool("resolve_program", {
501
+ registerTool("resolve_program", {
433
502
  title: "Resolve Program",
434
503
  description: "Resolve partial name/handle text to likely program_ids. Use this when the user gives a program name fragment like Nado.",
435
504
  inputSchema: {
@@ -453,7 +522,7 @@ export function createBbradarServer(client, config) {
453
522
  },
454
523
  annotations: readOnlyAnnotations
455
524
  }, (args) => runTool("resolve_program", config, rateLimiter, async () => resolveProgram(client, config, args)));
456
- server.registerTool("run_program_name_action", {
525
+ registerTool("run_program_name_action", {
457
526
  title: "Run Program Name Action",
458
527
  description: "Start here for one named program. Resolves a partial program name, then runs targets, new_targets, scope_delta, brief, or target_breakdown.",
459
528
  inputSchema: {
@@ -467,8 +536,8 @@ export function createBbradarServer(client, config) {
467
536
  language_tags: languageTagListSchema(),
468
537
  include_out_of_scope: z.boolean().default(false),
469
538
  include_ineligible: z.boolean().default(false),
470
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
471
- max_targets: acceptedTargetLimitSchema.default(25),
539
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
540
+ max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
472
541
  group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
473
542
  target_list_mode: targetListModeSchema.default("identifiers"),
474
543
  max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
@@ -487,7 +556,7 @@ export function createBbradarServer(client, config) {
487
556
  },
488
557
  annotations: readOnlyAnnotations
489
558
  }, (args) => runTool("run_program_name_action", config, rateLimiter, async () => runProgramNameAction(client, config, args)));
490
- server.registerTool("get_program_eligible_targets_by_name", {
559
+ registerTool("get_program_eligible_targets_by_name", {
491
560
  title: "Get Eligible Targets By Program Name",
492
561
  description: "Fast one-call path for user asks like 'find in-scope eligible targets for Aztec': resolve the program name, fetch targets, and return filtered in-scope bounty-eligible targets. Prefer this over separate resolve_program + get_program_targets calls when the user asks for a target list by name.",
493
562
  inputSchema: {
@@ -511,6 +580,7 @@ export function createBbradarServer(client, config) {
511
580
  resolved_program: jsonRecordSchema().optional(),
512
581
  program_id: z.string().optional(),
513
582
  targets: z.array(z.unknown()).optional(),
583
+ continuation: jsonRecordSchema().optional(),
514
584
  warnings: z.array(z.string()).optional(),
515
585
  upstream_budget: jsonRecordSchema().optional(),
516
586
  ranking_scope: jsonRecordSchema().optional(),
@@ -518,16 +588,21 @@ export function createBbradarServer(client, config) {
518
588
  },
519
589
  annotations: readOnlyAnnotations
520
590
  }, (args) => runTool("get_program_eligible_targets_by_name", config, rateLimiter, async () => getProgramEligibleTargetsByName(client, config, args)));
521
- server.registerTool("get_program_targets", {
591
+ registerTool("get_program_targets", {
522
592
  title: "Get Program Targets",
523
- description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${MAX_TARGETS_PER_PROGRAM}.`,
593
+ description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${maxProgramTargetsPageSize}.`,
524
594
  inputSchema: {
525
595
  program_id: programIdentifierSchema,
596
+ search: searchTextSchema.optional(),
597
+ target_type: filterValueSchema().optional(),
598
+ min_target_score: z.number().int().min(0).max(100).optional(),
599
+ target_opportunity_label: targetOpportunityLabelSchema.optional(),
600
+ competition_risk: competitionRiskSchema.optional(),
526
601
  include_out_of_scope: z.boolean().default(false),
527
602
  include_ineligible: z.boolean().default(false),
528
603
  strict_scope_filter: z.boolean().default(true),
529
604
  offset: z.number().int().min(0).default(0),
530
- limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
605
+ limit: z.number().int().min(1).max(maxProgramTargetsPageSize).default(Math.min(100, maxProgramTargetsPageSize)),
531
606
  output_mode: targetListModeSchema.default("compact")
532
607
  },
533
608
  outputSchema: {
@@ -538,13 +613,21 @@ export function createBbradarServer(client, config) {
538
613
  },
539
614
  annotations: readOnlyAnnotations
540
615
  }, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
541
- return runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
616
+ const payload = await runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
617
+ return applyCollectionContinuation(payload, "targets", "program targets", continuationStore, config);
542
618
  }));
543
- server.registerTool("get_recent_changes", {
619
+ registerTool("get_recent_changes", {
544
620
  title: "Get Recent Target Changes",
545
621
  description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
546
622
  inputSchema: {
547
623
  change_type: z.enum(["added", "updated", "removed"]).optional(),
624
+ after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
625
+ resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
626
+ max_cursor_pages: z.number().int().min(1).max(10).default(2),
627
+ cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
628
+ min_target_score: z.number().int().min(0).max(100).optional(),
629
+ target_opportunity_label: targetOpportunityLabelSchema.optional(),
630
+ competition_risk: competitionRiskSchema.optional(),
548
631
  include_removed: z.boolean().default(false),
549
632
  include_ineligible: z.boolean().default(false),
550
633
  include_out_of_scope: z.boolean().default(false),
@@ -559,35 +642,119 @@ export function createBbradarServer(client, config) {
559
642
  ...apiEnvelopeOutputShape(),
560
643
  changes: z.array(jsonRecordSchema()).optional(),
561
644
  meta: jsonRecordSchema().optional(),
562
- facets: jsonRecordSchema().optional()
645
+ facets: jsonRecordSchema().optional(),
646
+ cursor: jsonRecordSchema().optional(),
647
+ continuation: jsonRecordSchema().optional()
563
648
  },
564
649
  annotations: readOnlyAnnotations
565
650
  }, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
566
- const { output_mode, ...filters } = args;
567
- const api = await client.getRecentChanges(toQuery(filters));
568
- const data = readObject(api.data);
651
+ const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
652
+ const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
653
+ const { min_target_score, target_opportunity_label, competition_risk, ...upstreamFilters } = filters;
654
+ const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRecentChanges(toQuery({
655
+ ...upstreamFilters,
656
+ minTargetScore: min_target_score,
657
+ targetOpportunityLabel: target_opportunity_label,
658
+ competitionRisk: competition_risk,
659
+ after_id: afterId,
660
+ page: afterId === undefined ? filters.page : 1
661
+ })));
662
+ const api = collected.lastApi;
663
+ const data = collected.lastData;
569
664
  const sanitizedMeta = readObject(sanitizeJson(data?.meta));
570
- const changes = readArray(data?.results)
665
+ const changes = collected.results
571
666
  .map((change) => sanitizeChange(change, config.webBaseUrl))
572
667
  .map((change) => formatChange(change, output_mode));
573
- return withApiMetadata(api, {
668
+ return applyCollectionContinuation(withApiMetadata(api, {
574
669
  changes,
575
670
  meta: stripUndefined({
576
671
  ...(sanitizedMeta ?? {}),
577
- output_mode
672
+ output_mode,
673
+ pages_fetched: collected.pagesFetched,
674
+ returned_across_pages: changes.length,
675
+ cursor_page_budget_exhausted: collected.pageBudgetExhausted,
676
+ cursor_time_budget_exhausted: collected.timeBudgetExhausted
578
677
  }),
579
- facets: sanitizeJson(data?.facets)
580
- });
678
+ facets: sanitizeJson(data?.facets),
679
+ cursor: collected.cursor,
680
+ source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
681
+ }), "changes", "target changes", continuationStore, config);
682
+ }));
683
+ registerTool("get_repository_changes", {
684
+ title: "Get Repository Changes",
685
+ description: "Read-only repository push/change feed. Defaults to lightweight program summaries; use after_id for efficient incremental polling. For rewarded Web3 repository targets, call this in parallel with find_programs using the same platforms, then intersect event program ids with the ranked rewarded programs.",
686
+ inputSchema: {
687
+ after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
688
+ resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
689
+ max_cursor_pages: z.number().int().min(1).max(10).default(2),
690
+ cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
691
+ include_targets: z.boolean().default(false).describe("Include matched targets. False is faster and returns affected programs only."),
692
+ search: searchTextSchema.optional(),
693
+ platforms: platformListSchema(),
694
+ tags: tagListSchema(),
695
+ free_submission_only: z.boolean().default(false),
696
+ include_removed: z.boolean().default(false),
697
+ include_ineligible: z.boolean().default(false),
698
+ include_out_of_scope: z.boolean().default(false),
699
+ page: pageSchema,
700
+ page_size: repositoryChangesPageSizeSchema,
701
+ output_mode: repositoryChangeListModeSchema.default("compact")
702
+ },
703
+ outputSchema: {
704
+ ...apiEnvelopeOutputShape(),
705
+ repository_changes: z.array(jsonRecordSchema()).optional(),
706
+ meta: jsonRecordSchema().optional(),
707
+ facets: jsonRecordSchema().optional(),
708
+ available_facets: jsonRecordSchema().optional(),
709
+ cursor: jsonRecordSchema().optional(),
710
+ continuation: jsonRecordSchema().optional()
711
+ },
712
+ annotations: readOnlyAnnotations
713
+ }, (args) => runTool("get_repository_changes", config, rateLimiter, async () => {
714
+ if (!client.supportsEndpoint("repository_changes")) {
715
+ throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
716
+ }
717
+ const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
718
+ const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
719
+ const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRepositoryChanges(toQuery({
720
+ ...filters,
721
+ after_id: afterId,
722
+ page: afterId === undefined ? filters.page : 1
723
+ })));
724
+ const api = collected.lastApi;
725
+ const data = collected.lastData;
726
+ return applyCollectionContinuation(withApiMetadata(api, {
727
+ repository_changes: collected.results.map((change) => sanitizeRepositoryChange(change, config.webBaseUrl)).map((change) => formatRepositoryChange(change, output_mode)),
728
+ meta: stripUndefined({
729
+ ...(readObject(sanitizeJson(data?.meta)) ?? {}),
730
+ output_mode,
731
+ pages_fetched: collected.pagesFetched,
732
+ returned_across_pages: collected.results.length,
733
+ cursor_page_budget_exhausted: collected.pageBudgetExhausted,
734
+ cursor_time_budget_exhausted: collected.timeBudgetExhausted
735
+ }),
736
+ facets: sanitizeJson(data?.facets),
737
+ available_facets: sanitizeJson(data?.available_facets),
738
+ cursor: collected.cursor,
739
+ source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
740
+ }), "repository_changes", "repository changes", continuationStore, config);
581
741
  }));
582
- server.registerTool("get_latest_added_targets", {
742
+ registerTool("get_latest_added_targets", {
583
743
  title: "Get Latest Added Targets",
584
- description: "Latest added targets by type, optionally with that program's target list.",
744
+ description: "Latest added targets by type with optional platform, reward, and report-count filters. For fast wildcard discovery use target_type=wildcard and include_full_target_list=false.",
585
745
  inputSchema: {
586
746
  target_type: filterValueSchema().default("domain"),
747
+ platforms: platformListSchema(),
748
+ min_reward: z.number().int().min(0).optional(),
749
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
750
+ max_public_report_count: z.number().int().min(0).optional(),
751
+ require_known_report_count: z.boolean().default(false),
752
+ include_unknown_report_count: z.boolean().default(true),
587
753
  include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
588
754
  include_ineligible: z.boolean().default(false).describe("Include ineligible."),
589
- recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
590
- include_full_target_list: z.boolean().default(true),
755
+ recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
756
+ max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
757
+ include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program's complete target list. Set false for the fastest cross-program discovery."),
591
758
  full_target_list_mode: targetListModeSchema.default("identifiers"),
592
759
  full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
593
760
  full_list_include_out_of_scope: z.boolean().default(false),
@@ -599,13 +766,14 @@ export function createBbradarServer(client, config) {
599
766
  query: jsonRecordSchema().optional(),
600
767
  latest_added_at: z.string().optional(),
601
768
  program: programOutputSchema().optional(),
769
+ matches: z.array(jsonRecordSchema()).optional(),
602
770
  new_targets: z.array(z.unknown()).optional(),
603
771
  full_targets: z.array(z.unknown()).optional(),
604
772
  meta: jsonRecordSchema().optional()
605
773
  },
606
774
  annotations: readOnlyAnnotations
607
775
  }, (args) => runTool("get_latest_added_targets", config, rateLimiter, async () => getLatestAddedTargets(client, config, args)));
608
- server.registerTool("get_program_scope_summary", {
776
+ registerTool("get_program_scope_summary", {
609
777
  title: "Get Program Scope Summary",
610
778
  description: "Grouped compact scope for one program.",
611
779
  inputSchema: {
@@ -624,7 +792,7 @@ export function createBbradarServer(client, config) {
624
792
  },
625
793
  annotations: readOnlyAnnotations
626
794
  }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeSummaryFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
627
- server.registerTool("get_program_target_breakdown", {
795
+ registerTool("get_program_target_breakdown", {
628
796
  title: "Get Program Target Breakdown",
629
797
  description: "Target type, scope tag, language tag, and bucket counts for one program.",
630
798
  inputSchema: {
@@ -643,7 +811,7 @@ export function createBbradarServer(client, config) {
643
811
  },
644
812
  annotations: readOnlyAnnotations
645
813
  }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramTargetBreakdownFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
646
- server.registerTool("get_program_scope_delta", {
814
+ registerTool("get_program_scope_delta", {
647
815
  title: "Get Program Scope Delta",
648
816
  description: "Compact recent scope changes for one program.",
649
817
  inputSchema: {
@@ -655,8 +823,8 @@ export function createBbradarServer(client, config) {
655
823
  include_removed: z.boolean().default(true),
656
824
  include_out_of_scope: z.boolean().default(false),
657
825
  include_ineligible: z.boolean().default(false),
658
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
659
- max_targets: acceptedTargetLimitSchema.default(50),
826
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
827
+ max_targets: acceptedTargetLimitSchema.default(Math.min(50, maxProgramTargetsPageSize)),
660
828
  target_list_mode: targetListModeSchema.default("identifiers")
661
829
  },
662
830
  outputSchema: {
@@ -669,7 +837,7 @@ export function createBbradarServer(client, config) {
669
837
  },
670
838
  annotations: readOnlyAnnotations
671
839
  }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeDeltaFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
672
- server.registerTool("get_recent_target_activity", {
840
+ registerTool("get_recent_target_activity", {
673
841
  title: "Get Recent Target Activity",
674
842
  description: "Recent target changes grouped by program.",
675
843
  inputSchema: {
@@ -682,7 +850,7 @@ export function createBbradarServer(client, config) {
682
850
  platforms: platformListSchema(),
683
851
  tags: tagListSchema(),
684
852
  language_tags: languageTagListSchema(),
685
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
853
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
686
854
  max_programs: z.number().int().min(1).max(50).default(10),
687
855
  sample_size: z.number().int().min(1).max(20).default(5),
688
856
  target_list_mode: targetListModeSchema.default("identifiers")
@@ -695,7 +863,7 @@ export function createBbradarServer(client, config) {
695
863
  },
696
864
  annotations: readOnlyAnnotations
697
865
  }, (args) => runTool("get_recent_target_activity", config, rateLimiter, async () => getRecentTargetActivity(client, config, args)));
698
- server.registerTool("check_program_new_targets", {
866
+ registerTool("check_program_new_targets", {
699
867
  title: "Check Program New Targets",
700
868
  description: "Yes/no new in-scope targets for one program.",
701
869
  inputSchema: {
@@ -703,8 +871,8 @@ export function createBbradarServer(client, config) {
703
871
  since: isoDateTimeSchema.optional().describe("ISO datetime."),
704
872
  target_type: filterValueSchema().optional(),
705
873
  include_ineligible: z.boolean().default(false).describe("Include ineligible."),
706
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
707
- max_targets: acceptedTargetLimitSchema.default(25),
874
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
875
+ max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
708
876
  target_list_mode: targetListModeSchema.default("identifiers")
709
877
  },
710
878
  outputSchema: {
@@ -720,7 +888,7 @@ export function createBbradarServer(client, config) {
720
888
  },
721
889
  annotations: readOnlyAnnotations
722
890
  }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => checkProgramNewTargets(client, config, { ...args, program_id: programId }), (indexFallback) => checkProgramNewTargetsFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
723
- server.registerTool("check_watchlist_new_targets", {
891
+ registerTool("check_watchlist_new_targets", {
724
892
  title: "Check Watchlist New Targets",
725
893
  description: "New in-scope targets across many known program_ids.",
726
894
  inputSchema: {
@@ -728,7 +896,7 @@ export function createBbradarServer(client, config) {
728
896
  since: isoDateTimeSchema.optional(),
729
897
  target_type: filterValueSchema().optional(),
730
898
  include_ineligible: z.boolean().default(false),
731
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
899
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
732
900
  max_targets_per_program: z.number().int().min(1).max(50).default(10),
733
901
  target_list_mode: targetListModeSchema.default("identifiers")
734
902
  },
@@ -741,7 +909,7 @@ export function createBbradarServer(client, config) {
741
909
  },
742
910
  annotations: readOnlyAnnotations
743
911
  }, (args) => runTool("check_watchlist_new_targets", config, rateLimiter, async () => checkWatchlistNewTargets(client, config, args)));
744
- server.registerTool("get_opportunities", {
912
+ registerTool("get_opportunities", {
745
913
  title: "Get BBRadar Opportunities",
746
914
  description: `List programs in one opportunity tier. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
747
915
  inputSchema: {
@@ -772,16 +940,18 @@ export function createBbradarServer(client, config) {
772
940
  meta: sanitizeJson(data?.meta)
773
941
  });
774
942
  }));
775
- server.registerTool("find_programs", {
943
+ registerTool("find_programs", {
776
944
  title: "Find BBRadar Programs",
777
- description: "Ranked program discovery with filters and compact output.",
945
+ 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.",
778
946
  inputSchema: {
779
947
  platforms: platformListSchema(),
948
+ 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."),
780
949
  tags: tagListSchema(),
781
950
  scope_tags: scopeTagListSchema(),
782
951
  target_types: targetTypeListSchema(),
783
952
  language_tags: languageTagListSchema(),
784
953
  web3: z.boolean().default(false).describe("Require Web3 signals."),
954
+ exclude_web3: z.boolean().default(false).describe("Exclude programs with Web3, blockchain, smart-contract, crypto, DeFi, NFT, Solidity, Ethereum, or Solana signals. Use this for Web2-only discovery."),
785
955
  opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
786
956
  updated_since: isoDateTimeSchema.optional(),
787
957
  min_bounty_min: z.number().int().min(0).optional(),
@@ -820,24 +990,30 @@ export function createBbradarServer(client, config) {
820
990
  ...args,
821
991
  target_samples_only_wildcards: args.min_wildcards > 0
822
992
  })));
823
- server.registerTool("find_wildcard_programs", {
993
+ registerTool("find_wildcard_programs", {
824
994
  title: "Find Wildcard Programs",
825
- description: "Wildcard program shortlist.",
995
+ description: "Wildcard program shortlist with platform, minimum reward, freshness, and report-count filters.",
826
996
  inputSchema: {
827
997
  platforms: platformListSchema(),
828
998
  tags: tagListSchema(),
829
999
  updated_since: isoDateTimeSchema.optional(),
1000
+ min_reward: z.number().int().min(0).optional(),
1001
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
830
1002
  max_public_report_count: z.number().int().min(0).default(25),
831
1003
  require_known_report_count: z.boolean().default(true),
832
1004
  include_unknown_report_count: z.boolean().default(false),
833
1005
  exclude_ctf: z.boolean().default(true),
834
1006
  require_bounty: z.boolean().default(true),
835
1007
  min_wildcards: z.number().int().min(1).max(100).default(1),
1008
+ min_added_24h: z.number().int().min(0).optional(),
1009
+ min_added_7d: z.number().int().min(0).optional(),
1010
+ sort_by: programSortSchema.default("best_hunt_value"),
836
1011
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
837
1012
  max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
838
1013
  include_target_samples: z.boolean().default(false),
839
1014
  target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
840
1015
  target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
1016
+ output_mode: programListModeSchema.default("compact"),
841
1017
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
842
1018
  },
843
1019
  outputSchema: findProgramsOutputShape(),
@@ -851,8 +1027,8 @@ export function createBbradarServer(client, config) {
851
1027
  web3: false,
852
1028
  opportunity_levels: [],
853
1029
  updated_since: args.updated_since,
854
- min_bounty_min: undefined,
855
- min_bounty_max: undefined,
1030
+ min_bounty_min: args.min_reward !== undefined && args.reward_threshold_mode === "min_at_least" ? args.min_reward : undefined,
1031
+ min_bounty_max: args.min_reward !== undefined && args.reward_threshold_mode === "max_at_least" ? args.min_reward : undefined,
856
1032
  max_bounty_max: undefined,
857
1033
  require_bounty: args.require_bounty,
858
1034
  max_public_report_count: args.max_public_report_count,
@@ -867,20 +1043,21 @@ export function createBbradarServer(client, config) {
867
1043
  min_wildcards: args.min_wildcards,
868
1044
  min_eligible_targets: undefined,
869
1045
  min_total_targets: undefined,
870
- min_added_24h: undefined,
871
- min_added_7d: undefined,
1046
+ min_added_24h: args.min_added_24h,
1047
+ min_added_7d: args.min_added_7d,
872
1048
  max_pages: args.max_pages,
873
1049
  max_results: args.max_results,
874
- sort_by: "best_hunt_value",
1050
+ sort_by: args.sort_by,
875
1051
  include_target_samples: args.include_target_samples,
876
1052
  target_sample_programs: args.target_sample_programs,
877
1053
  target_sample_size: args.target_sample_size,
878
1054
  only_in_scope_targets: true,
879
1055
  only_bounty_eligible_targets: true,
880
1056
  target_samples_only_wildcards: true,
1057
+ output_mode: args.output_mode,
881
1058
  upstream_request_budget: args.upstream_request_budget
882
1059
  })));
883
- server.registerTool("find_web3_programs", {
1060
+ registerTool("find_web3_programs", {
884
1061
  title: "Find Web3 Programs",
885
1062
  description: "Web3/blockchain/smart-contract program shortlist.",
886
1063
  inputSchema: {
@@ -941,7 +1118,7 @@ export function createBbradarServer(client, config) {
941
1118
  target_samples_only_wildcards: args.min_wildcards > 0,
942
1119
  upstream_request_budget: args.upstream_request_budget
943
1120
  })));
944
- server.registerTool("find_reward_programs", {
1121
+ registerTool("find_reward_programs", {
945
1122
  title: "Find Reward Programs",
946
1123
  description: "Programs matching reward thresholds.",
947
1124
  inputSchema: {
@@ -970,7 +1147,7 @@ export function createBbradarServer(client, config) {
970
1147
  outputSchema: findProgramsOutputShape(),
971
1148
  annotations: readOnlyAnnotations
972
1149
  }, (args) => runTool("find_reward_programs", config, rateLimiter, async () => findRewardPrograms(client, config, args)));
973
- server.registerTool("find_web3_contests", {
1150
+ registerTool("find_web3_contests", {
974
1151
  title: "Find Web3 Contests",
975
1152
  description: "Web3 contest or competitive audit shortlist.",
976
1153
  inputSchema: {
@@ -993,7 +1170,7 @@ export function createBbradarServer(client, config) {
993
1170
  outputSchema: findProgramsOutputShape(),
994
1171
  annotations: readOnlyAnnotations
995
1172
  }, (args) => runTool("find_web3_contests", config, rateLimiter, async () => findWeb3Contests(client, config, args)));
996
- server.registerTool("find_language_programs", {
1173
+ registerTool("find_language_programs", {
997
1174
  title: "Find Language Programs",
998
1175
  description: "Language-specific program shortlist.",
999
1176
  inputSchema: {
@@ -1020,7 +1197,7 @@ export function createBbradarServer(client, config) {
1020
1197
  outputSchema: findProgramsOutputShape(),
1021
1198
  annotations: readOnlyAnnotations
1022
1199
  }, (args) => runTool("find_language_programs", config, rateLimiter, async () => findLanguagePrograms(client, config, args)));
1023
- server.registerTool("find_target_type_programs", {
1200
+ registerTool("find_target_type_programs", {
1024
1201
  title: "Find Target Type Programs",
1025
1202
  description: "Target-type program shortlist.",
1026
1203
  inputSchema: {
@@ -1046,7 +1223,7 @@ export function createBbradarServer(client, config) {
1046
1223
  outputSchema: findProgramsOutputShape(),
1047
1224
  annotations: readOnlyAnnotations
1048
1225
  }, (args) => runTool("find_target_type_programs", config, rateLimiter, async () => findTargetTypePrograms(client, config, args)));
1049
- server.registerTool("find_vdp_programs", {
1226
+ registerTool("find_vdp_programs", {
1050
1227
  title: "Find VDP Programs",
1051
1228
  description: "VDP/disclosure-only/no-bounty program shortlist.",
1052
1229
  inputSchema: {
@@ -1066,7 +1243,7 @@ export function createBbradarServer(client, config) {
1066
1243
  outputSchema: findProgramsOutputShape(),
1067
1244
  annotations: readOnlyAnnotations
1068
1245
  }, (args) => runTool("find_vdp_programs", config, rateLimiter, async () => findVdpPrograms(client, config, args)));
1069
- server.registerTool("find_paid_programs", {
1246
+ registerTool("find_paid_programs", {
1070
1247
  title: "Find Paid Programs",
1071
1248
  description: "Paid bounty program shortlist.",
1072
1249
  inputSchema: {
@@ -1089,7 +1266,7 @@ export function createBbradarServer(client, config) {
1089
1266
  outputSchema: findProgramsOutputShape(),
1090
1267
  annotations: readOnlyAnnotations
1091
1268
  }, (args) => runTool("find_paid_programs", config, rateLimiter, async () => findPaidPrograms(client, config, args)));
1092
- server.registerTool("find_stack_matches", {
1269
+ registerTool("find_stack_matches", {
1093
1270
  title: "Find Stack Matches",
1094
1271
  description: "Combined stack/language/type/reward shortlist.",
1095
1272
  inputSchema: {
@@ -1119,7 +1296,7 @@ export function createBbradarServer(client, config) {
1119
1296
  outputSchema: findProgramsOutputShape(),
1120
1297
  annotations: readOnlyAnnotations
1121
1298
  }, (args) => runTool("find_stack_matches", config, rateLimiter, async () => findStackMatches(client, config, args)));
1122
- server.registerTool("find_low_noise_programs", {
1299
+ registerTool("find_low_noise_programs", {
1123
1300
  title: "Find Low Noise Programs",
1124
1301
  description: "Low-public-report program shortlist.",
1125
1302
  inputSchema: {
@@ -1145,7 +1322,7 @@ export function createBbradarServer(client, config) {
1145
1322
  outputSchema: findProgramsOutputShape(),
1146
1323
  annotations: readOnlyAnnotations
1147
1324
  }, (args) => runTool("find_low_noise_programs", config, rateLimiter, async () => findLowNoisePrograms(client, config, args)));
1148
- server.registerTool("find_recent_by_type", {
1325
+ registerTool("find_recent_by_type", {
1149
1326
  title: "Find Recent By Type",
1150
1327
  description: "Recent changes by target type grouped by program.",
1151
1328
  inputSchema: {
@@ -1158,7 +1335,7 @@ export function createBbradarServer(client, config) {
1158
1335
  platforms: platformListSchema(),
1159
1336
  tags: tagListSchema(),
1160
1337
  language_tags: languageTagListSchema(),
1161
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
1338
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
1162
1339
  max_programs: z.number().int().min(1).max(50).default(10),
1163
1340
  sample_size: z.number().int().min(1).max(20).default(5),
1164
1341
  target_list_mode: targetListModeSchema.default("identifiers")
@@ -1171,7 +1348,7 @@ export function createBbradarServer(client, config) {
1171
1348
  },
1172
1349
  annotations: readOnlyAnnotations
1173
1350
  }, (args) => runTool("find_recent_by_type", config, rateLimiter, async () => findRecentByType(client, config, args)));
1174
- server.registerTool("find_programs_by_target", {
1351
+ registerTool("find_programs_by_target", {
1175
1352
  title: "Find Programs By Target",
1176
1353
  description: "Reverse lookup target/domain ownership. Accepts domains, URLs, wildcards, target names, and partial target text.",
1177
1354
  inputSchema: {
@@ -1194,21 +1371,21 @@ export function createBbradarServer(client, config) {
1194
1371
  },
1195
1372
  annotations: readOnlyAnnotations
1196
1373
  }, (args) => runTool("find_programs_by_target", config, rateLimiter, async () => findProgramsByTarget(client, config, args)));
1197
- server.registerTool("find_program_candidates", {
1374
+ registerTool("find_program_candidates", {
1198
1375
  title: "Find BBRadar Program Candidates",
1199
1376
  description: "Start here for broad program discovery. Default ranked hunt-candidate preset with safe filters and compact output.",
1200
1377
  inputSchema: programCandidatePresetInputSchema,
1201
1378
  outputSchema: findProgramsOutputShape(),
1202
1379
  annotations: readOnlyAnnotations
1203
1380
  }, (args) => runTool("find_program_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
1204
- server.registerTool("find_hunt_candidates", {
1381
+ registerTool("find_hunt_candidates", {
1205
1382
  title: "Find BBRadar Hunt Candidates",
1206
1383
  description: "Legacy alias for find_program_candidates. Prefer find_program_candidates for new calls.",
1207
1384
  inputSchema: programCandidatePresetInputSchema,
1208
1385
  outputSchema: findProgramsOutputShape(),
1209
1386
  annotations: readOnlyAnnotations
1210
1387
  }, (args) => runTool("find_hunt_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
1211
- server.registerTool("find_fresh_hunt_candidates", {
1388
+ registerTool("find_fresh_hunt_candidates", {
1212
1389
  title: "Find Fresh Hunt Candidates",
1213
1390
  description: "Freshness-first hunt shortlist.",
1214
1391
  inputSchema: {
@@ -1277,7 +1454,7 @@ export function createBbradarServer(client, config) {
1277
1454
  output_mode: args.output_mode,
1278
1455
  upstream_request_budget: args.upstream_request_budget
1279
1456
  })));
1280
- server.registerTool("find_recently_updated_programs", {
1457
+ registerTool("find_recently_updated_programs", {
1281
1458
  title: "Find Recently Updated Programs",
1282
1459
  description: "Recently updated program shortlist across all platforms by default. Platform filters are applied only when explicitly provided.",
1283
1460
  inputSchema: {
@@ -1302,7 +1479,7 @@ export function createBbradarServer(client, config) {
1302
1479
  outputSchema: findProgramsOutputShape(),
1303
1480
  annotations: readOnlyAnnotations
1304
1481
  }, (args) => runTool("find_recently_updated_programs", config, rateLimiter, async () => findRecentlyUpdatedPrograms(client, config, args)));
1305
- server.registerTool("list_filters", {
1482
+ registerTool("list_filters", {
1306
1483
  title: "List BBRadar Filters",
1307
1484
  description: "Discover observed filter values.",
1308
1485
  inputSchema: {
@@ -1321,7 +1498,7 @@ export function createBbradarServer(client, config) {
1321
1498
  },
1322
1499
  annotations: readOnlyAnnotations
1323
1500
  }, (args) => runTool("list_filters", config, rateLimiter, async () => listFilters(client, args)));
1324
- server.registerTool("get_bbradar_guide", {
1501
+ registerTool("get_bbradar_guide", {
1325
1502
  title: "Get BBRadar Tool Guide",
1326
1503
  description: "Local quick-start guide for choosing BBRadar tools. Call this when the user's request is ambiguous or when a smaller model may need a decision tree.",
1327
1504
  inputSchema: {
@@ -1334,9 +1511,9 @@ export function createBbradarServer(client, config) {
1334
1511
  annotations: readOnlyAnnotations
1335
1512
  }, (args) => runTool("get_bbradar_guide", config, rateLimiter, async () => ({
1336
1513
  request_id: randomUUID(),
1337
- guide: bbradarGuide(args.topic)
1514
+ guide: bbradarGuide(args.topic, config.toolProfile, client)
1338
1515
  })));
1339
- server.registerTool("get_mcp_status", {
1516
+ registerTool("get_mcp_status", {
1340
1517
  title: "Get BBRadar MCP Status",
1341
1518
  description: "Local MCP status and timing metrics.",
1342
1519
  inputSchema: {},
@@ -1355,11 +1532,22 @@ export function createBbradarServer(client, config) {
1355
1532
  web_base_url: config.webBaseUrl,
1356
1533
  response_cache_enabled: config.cacheTtlMs > 0 && config.cacheMaxEntries > 0,
1357
1534
  cache_ttl_ms: config.cacheTtlMs,
1535
+ negative_cache_ttl_ms: config.negativeCacheTtlMs,
1358
1536
  cache_max_entries: config.cacheMaxEntries,
1359
1537
  local_program_index_enabled: false,
1360
1538
  default_rate_limit_per_minute: config.defaultRateLimitPerMinute,
1361
1539
  export_rate_limit_per_minute: config.exportRateLimitPerMinute,
1540
+ max_concurrent_upstream_requests: config.maxConcurrentRequests,
1541
+ compact_tool_text: config.compactToolText,
1542
+ tool_profile: config.toolProfile,
1362
1543
  request_timeout_ms: config.requestTimeoutMs,
1544
+ feed_cursors: feedCursorStore.stats(),
1545
+ continuations: {
1546
+ entries: continuationStore.size,
1547
+ max_entries: MAX_LOCAL_EXPORT_RESOURCES,
1548
+ inline_limit: config.continuationInlineLimit ?? 50,
1549
+ ttl_ms: config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS
1550
+ },
1363
1551
  validate_on_startup: config.validateOnStartup,
1364
1552
  transport: "stdio",
1365
1553
  remote_streamable_http: "not_enabled_for_local_setup",
@@ -1367,7 +1555,7 @@ export function createBbradarServer(client, config) {
1367
1555
  client: client.diagnostics()
1368
1556
  }
1369
1557
  })));
1370
- server.registerTool("compare_programs", {
1558
+ registerTool("compare_programs", {
1371
1559
  title: "Compare BBRadar Programs",
1372
1560
  description: "Compare programs.",
1373
1561
  inputSchema: {
@@ -1381,7 +1569,7 @@ export function createBbradarServer(client, config) {
1381
1569
  outputSchema: decisionOutputShape(),
1382
1570
  annotations: readOnlyAnnotations
1383
1571
  }, (args) => runTool("compare_programs", config, rateLimiter, async () => comparePrograms(client, config, args)));
1384
- server.registerTool("compare_programs_compact", {
1572
+ registerTool("compare_programs_compact", {
1385
1573
  title: "Compare Programs Compact",
1386
1574
  description: "Compact program comparison.",
1387
1575
  inputSchema: {
@@ -1394,7 +1582,7 @@ export function createBbradarServer(client, config) {
1394
1582
  outputSchema: decisionOutputShape(),
1395
1583
  annotations: readOnlyAnnotations
1396
1584
  }, (args) => runTool("compare_programs_compact", config, rateLimiter, async () => comparePrograms(client, config, { ...args, output_mode: "compact" })));
1397
- server.registerTool("summarize_program_activity", {
1585
+ registerTool("summarize_program_activity", {
1398
1586
  title: "Summarize Program Activity",
1399
1587
  description: "Summarize one program's activity.",
1400
1588
  inputSchema: {
@@ -1409,7 +1597,7 @@ export function createBbradarServer(client, config) {
1409
1597
  },
1410
1598
  annotations: readOnlyAnnotations
1411
1599
  }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
1412
- server.registerTool("get_program_brief", {
1600
+ registerTool("get_program_brief", {
1413
1601
  title: "Get Program Brief",
1414
1602
  description: "Compact worth-hunting brief for one program.",
1415
1603
  inputSchema: {
@@ -1432,17 +1620,28 @@ export function createBbradarServer(client, config) {
1432
1620
  },
1433
1621
  annotations: readOnlyAnnotations
1434
1622
  }, (args) => runTool("get_program_brief", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramBrief(client, config, { ...args, program_id: programId }))));
1435
- server.registerTool("find_recently_added_wildcards", {
1623
+ registerTool("find_recently_added_wildcards", {
1436
1624
  title: "Find Recently Added Wildcards",
1437
- description: "Recently added wildcard programs.",
1625
+ description: "Recently added wildcard programs with platform, minimum reward, and report-count filters. Returns bounded wildcard target samples.",
1438
1626
  inputSchema: {
1439
1627
  platforms: platformListSchema(),
1440
1628
  tags: tagListSchema(),
1629
+ min_added_24h: z.number().int().min(0).optional(),
1441
1630
  min_added_7d: z.number().int().min(1).default(1),
1631
+ min_reward: z.number().int().min(0).optional(),
1632
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
1442
1633
  max_public_report_count: z.number().int().min(0).optional(),
1634
+ require_known_report_count: z.boolean().default(false),
1635
+ include_unknown_report_count: z.boolean().default(false),
1443
1636
  require_bounty: z.boolean().default(true),
1637
+ min_wildcards: z.number().int().min(1).max(100).default(1),
1638
+ sort_by: programSortSchema.default("most_new_targets"),
1444
1639
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1445
1640
  max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
1641
+ include_target_samples: z.boolean().default(true),
1642
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1643
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
1644
+ output_mode: programListModeSchema.default("compact"),
1446
1645
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
1447
1646
  },
1448
1647
  outputSchema: findProgramsOutputShape(),
@@ -1456,37 +1655,38 @@ export function createBbradarServer(client, config) {
1456
1655
  web3: false,
1457
1656
  opportunity_levels: [],
1458
1657
  updated_since: undefined,
1459
- min_bounty_min: undefined,
1460
- min_bounty_max: undefined,
1658
+ min_bounty_min: args.min_reward !== undefined && args.reward_threshold_mode === "min_at_least" ? args.min_reward : undefined,
1659
+ min_bounty_max: args.min_reward !== undefined && args.reward_threshold_mode === "max_at_least" ? args.min_reward : undefined,
1461
1660
  max_bounty_max: undefined,
1462
1661
  require_bounty: args.require_bounty,
1463
1662
  max_public_report_count: args.max_public_report_count,
1464
- require_known_report_count: false,
1465
- include_unknown_report_count: false,
1663
+ require_known_report_count: args.require_known_report_count,
1664
+ include_unknown_report_count: args.include_unknown_report_count,
1466
1665
  exclude_ctf: true,
1467
1666
  exclude_mobile_only: false,
1468
1667
  exclude_non_web: false,
1469
1668
  has_api_scope: false,
1470
1669
  fresh_launch_days: undefined,
1471
- min_wildcards: 1,
1670
+ min_wildcards: args.min_wildcards,
1472
1671
  min_eligible_targets: undefined,
1473
1672
  min_total_targets: undefined,
1474
- min_added_24h: undefined,
1673
+ min_added_24h: args.min_added_24h,
1475
1674
  min_added_7d: args.min_added_7d,
1476
1675
  max_pages: args.max_pages,
1477
1676
  max_results: args.max_results,
1478
- sort_by: "best_hunt_value",
1479
- include_target_samples: true,
1480
- target_sample_programs: Math.min(args.max_results, MAX_FIND_TARGET_SAMPLE_PROGRAMS),
1481
- target_sample_size: 5,
1677
+ sort_by: args.sort_by,
1678
+ include_target_samples: args.include_target_samples,
1679
+ target_sample_programs: args.target_sample_programs,
1680
+ target_sample_size: args.target_sample_size,
1482
1681
  only_in_scope_targets: true,
1483
1682
  only_bounty_eligible_targets: true,
1484
1683
  target_samples_only_wildcards: true,
1684
+ output_mode: args.output_mode,
1485
1685
  upstream_request_budget: args.upstream_request_budget
1486
1686
  })));
1487
- server.registerTool("find_low_competition_high_reward", {
1687
+ registerTool("find_low_competition_high_reward", {
1488
1688
  title: "Find Low Competition High Reward",
1489
- description: "Legacy low-report high-reward shortlist.",
1689
+ description: "Low-report, high-reward shortlist with optional wildcard and technology filters.",
1490
1690
  inputSchema: {
1491
1691
  platforms: platformListSchema(),
1492
1692
  tags: tagListSchema(),
@@ -1495,6 +1695,7 @@ export function createBbradarServer(client, config) {
1495
1695
  max_public_report_count: z.number().int().min(0).default(10),
1496
1696
  min_bounty_max: z.number().int().min(0).default(5_000),
1497
1697
  min_eligible_targets: z.number().int().min(0).default(1),
1698
+ min_wildcards: z.number().int().min(0).max(100).default(0),
1498
1699
  exclude_ctf: z.boolean().default(true),
1499
1700
  exclude_mobile_only: z.boolean().default(false),
1500
1701
  exclude_non_web: z.boolean().default(false),
@@ -1502,6 +1703,10 @@ export function createBbradarServer(client, config) {
1502
1703
  fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
1503
1704
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1504
1705
  max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
1706
+ include_target_samples: z.boolean().default(true),
1707
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1708
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
1709
+ output_mode: programListModeSchema.default("compact"),
1505
1710
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
1506
1711
  },
1507
1712
  outputSchema: findProgramsOutputShape(),
@@ -1527,7 +1732,7 @@ export function createBbradarServer(client, config) {
1527
1732
  exclude_non_web: args.exclude_non_web,
1528
1733
  has_api_scope: args.has_api_scope,
1529
1734
  fresh_launch_days: args.fresh_launch_days,
1530
- min_wildcards: 0,
1735
+ min_wildcards: args.min_wildcards,
1531
1736
  min_eligible_targets: args.min_eligible_targets,
1532
1737
  min_total_targets: undefined,
1533
1738
  min_added_24h: undefined,
@@ -1535,15 +1740,16 @@ export function createBbradarServer(client, config) {
1535
1740
  max_pages: args.max_pages,
1536
1741
  max_results: args.max_results,
1537
1742
  sort_by: "best_hunt_value",
1538
- include_target_samples: true,
1539
- target_sample_programs: Math.min(args.max_results, MAX_FIND_TARGET_SAMPLE_PROGRAMS),
1540
- target_sample_size: 5,
1743
+ include_target_samples: args.include_target_samples,
1744
+ target_sample_programs: args.target_sample_programs,
1745
+ target_sample_size: args.target_sample_size,
1541
1746
  only_in_scope_targets: true,
1542
1747
  only_bounty_eligible_targets: true,
1543
- target_samples_only_wildcards: false,
1748
+ target_samples_only_wildcards: args.min_wildcards > 0,
1749
+ output_mode: args.output_mode,
1544
1750
  upstream_request_budget: args.upstream_request_budget
1545
1751
  })));
1546
- server.registerTool("get_program_delta", {
1752
+ registerTool("get_program_delta", {
1547
1753
  title: "Get Program Delta",
1548
1754
  description: "Recent target changes for one program.",
1549
1755
  inputSchema: {
@@ -1551,7 +1757,7 @@ export function createBbradarServer(client, config) {
1551
1757
  include_removed: z.boolean().default(true),
1552
1758
  include_ineligible: z.boolean().default(false),
1553
1759
  include_out_of_scope: z.boolean().default(false),
1554
- page_size: recentChangesPageSizeSchema.default(50)
1760
+ page_size: recentChangesPageSizeSchema.default(Math.min(50, maxTargetChangesPageSize))
1555
1761
  },
1556
1762
  outputSchema: {
1557
1763
  ...apiEnvelopeOutputShape(),
@@ -1561,18 +1767,18 @@ export function createBbradarServer(client, config) {
1561
1767
  },
1562
1768
  annotations: readOnlyAnnotations
1563
1769
  }, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
1564
- server.registerTool("export_targets", {
1770
+ registerTool("export_targets", {
1565
1771
  title: "Export BBRadar Targets",
1566
- description: `Read-only target export. limit max ${MAX_TARGET_EXPORT}.`,
1772
+ description: `Read-only target export. limit max ${maxTargetExport}.`,
1567
1773
  inputSchema: {
1568
- program_ids: z.array(programIdentifierSchema).max(50).default([]),
1774
+ program_ids: z.array(programIdentifierSchema).max(maxTargetExportProgramIds).default([]),
1569
1775
  platforms: platformListSchema(),
1570
1776
  tags: tagListSchema(),
1571
1777
  opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
1572
1778
  include_out_of_scope: z.boolean().default(false),
1573
1779
  include_ineligible: z.boolean().default(false),
1574
1780
  format: z.enum(["json", "csv"]).default("json"),
1575
- limit: z.number().int().min(1).max(MAX_TARGET_EXPORT).default(MAX_TARGET_EXPORT)
1781
+ limit: z.number().int().min(1).max(maxTargetExport).default(maxTargetExport)
1576
1782
  },
1577
1783
  outputSchema: {
1578
1784
  ...apiEnvelopeOutputShape(),
@@ -1618,11 +1824,14 @@ export function createBbradarServer(client, config) {
1618
1824
  });
1619
1825
  }
1620
1826
  }));
1621
- registerPrompts(server);
1622
- registerResources(server, client, config, exportStore);
1827
+ registerPrompts(server, config.toolProfile);
1828
+ registerResources(server, client, config, exportStore, continuationStore);
1623
1829
  return server;
1624
1830
  }
1625
- function bbradarGuide(topic) {
1831
+ function bbradarGuide(topic, profile, client) {
1832
+ if (profile === "core") {
1833
+ return coreBbradarGuide(topic, client);
1834
+ }
1626
1835
  const primaryEntryPoints = [
1627
1836
  {
1628
1837
  tool: "find_program_candidates",
@@ -1654,6 +1863,11 @@ function bbradarGuide(topic) {
1654
1863
  first_tool: "find_program_candidates",
1655
1864
  then: "Use get_program_brief on the best program_id if the user wants details."
1656
1865
  },
1866
+ {
1867
+ user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
1868
+ first_tool: "list_filters",
1869
+ 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."
1870
+ },
1657
1871
  {
1658
1872
  user_goal: "named program summary",
1659
1873
  first_tool: "run_program_name_action",
@@ -1674,6 +1888,43 @@ function bbradarGuide(topic) {
1674
1888
  first_tool: "find_recently_updated_programs",
1675
1889
  then: "Use get_latest_added_targets for global added targets, or check_program_new_targets when the user names one program."
1676
1890
  },
1891
+ {
1892
+ user_goal: "repository changes or repository polling",
1893
+ first_tool: "get_repository_changes",
1894
+ arguments: { include_targets: false, max_cursor_pages: 2 },
1895
+ then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
1896
+ },
1897
+ {
1898
+ user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
1899
+ first_tool: "get_repository_changes",
1900
+ arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
1901
+ parallel_with: {
1902
+ tool: "find_programs",
1903
+ arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
1904
+ },
1905
+ then: "Intersect repository-event program ids with ranked program ids. Targets belong to the repository event association even when target rows omit program_id; keep strong target scores/labels and only the requested program platforms."
1906
+ },
1907
+ {
1908
+ user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
1909
+ first_tool: "get_latest_added_targets",
1910
+ arguments: {
1911
+ target_type: "wildcard",
1912
+ platforms: ["requested platform"],
1913
+ min_reward: 5000,
1914
+ max_public_report_count: 10,
1915
+ include_full_target_list: false
1916
+ }
1917
+ },
1918
+ {
1919
+ user_goal: "wildcard programs with low report count and good rewards",
1920
+ first_tool: "find_wildcard_programs",
1921
+ arguments: {
1922
+ platforms: ["requested platform"],
1923
+ min_reward: 5000,
1924
+ max_public_report_count: 10,
1925
+ include_target_samples: true
1926
+ }
1927
+ },
1677
1928
  {
1678
1929
  user_goal: "compare a short list",
1679
1930
  first_tool: "compare_programs_compact",
@@ -1691,17 +1942,19 @@ function bbradarGuide(topic) {
1691
1942
  }
1692
1943
  ];
1693
1944
  const advancedTools = {
1694
- discovery: ["find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
1695
- freshness: ["find_recently_updated_programs", "find_fresh_hunt_candidates", "get_latest_added_targets", "get_recent_target_activity", "get_program_scope_delta", "check_watchlist_new_targets"],
1945
+ 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"],
1946
+ 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"],
1696
1947
  details: ["get_program_brief", "get_program_scope_summary", "get_program_target_breakdown", "find_programs_by_target"],
1697
1948
  exports: ["export_targets"]
1698
1949
  };
1699
1950
  const filters = {
1700
1951
  platforms: "Only set when the user explicitly names a platform. Do not infer Immunefi, HackerOne, etc.",
1952
+ exclude_platforms: "Use exact observed values from list_filters for user-requested platform or geographic exclusions.",
1701
1953
  tags: "Broad combined tag filter across scope, language, and target-type signals.",
1702
1954
  scope_tags: "Scope category signals such as domain, api, mobile, smart-contract, wildcard.",
1703
1955
  target_types: "Target-kind signals such as domain, api, url, mobile, smart-contract.",
1704
1956
  language_tags: "Technology/language signals such as javascript, solidity, web3, php, python.",
1957
+ exclude_web3: "Set true for Web2-only requests.",
1705
1958
  vdp_mode: "Default exclude. Include VDP/no-bounty data only when asked."
1706
1959
  };
1707
1960
  const examples = [
@@ -1724,6 +1977,20 @@ function bbradarGuide(topic) {
1724
1977
  {
1725
1978
  user_says: "who owns https://api.example.com/v1/users",
1726
1979
  call: { tool: "find_programs_by_target", arguments: { target_query: "https://api.example.com/v1/users" } }
1980
+ },
1981
+ {
1982
+ user_says: "latest HackerOne wildcard targets with rewards over 5000",
1983
+ call: {
1984
+ tool: "get_latest_added_targets",
1985
+ arguments: { target_type: "wildcard", platforms: ["HackerOne"], min_reward: 5000, include_full_target_list: false }
1986
+ }
1987
+ },
1988
+ {
1989
+ user_says: "wildcard programs with under 10 reports and good rewards",
1990
+ call: {
1991
+ tool: "find_wildcard_programs",
1992
+ arguments: { max_public_report_count: 10, min_reward: 5000, include_target_samples: true }
1993
+ }
1727
1994
  }
1728
1995
  ];
1729
1996
  return {
@@ -1738,12 +2005,99 @@ function bbradarGuide(topic) {
1738
2005
  require_bounty: true
1739
2006
  },
1740
2007
  primary_entry_points: topic === "start" ? primaryEntryPoints : primaryEntryPoints.filter((entry) => entry.tool !== "get_bbradar_guide"),
1741
- decision_tree: decisionTree.filter((entry) => topic === "start" || guideEntryMatchesTopic(topic, entry)),
2008
+ decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
2009
+ (topic === "start" || guideEntryMatchesTopic(topic, entry))),
1742
2010
  filters,
1743
2011
  advanced_tools: advancedTools,
1744
2012
  examples
1745
2013
  };
1746
2014
  }
2015
+ function coreBbradarGuide(topic, client) {
2016
+ const decisionTree = [
2017
+ {
2018
+ user_goal: "find programs to hunt",
2019
+ first_tool: "find_programs",
2020
+ arguments: { max_results: 10, output_mode: "compact" }
2021
+ },
2022
+ {
2023
+ user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
2024
+ first_tool: "list_filters",
2025
+ then: "Resolve 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. Relax one local threshold on an empty result; cached source pages make retries fast."
2026
+ },
2027
+ {
2028
+ user_goal: "named program summary or action",
2029
+ first_tool: "run_program_name_action",
2030
+ arguments: { query: "program name", action: "brief" }
2031
+ },
2032
+ {
2033
+ user_goal: "named program targets",
2034
+ first_tool: "get_program_eligible_targets_by_name",
2035
+ arguments: { query: "program name", target_list_mode: "identifiers" }
2036
+ },
2037
+ {
2038
+ user_goal: "find which program owns a target",
2039
+ first_tool: "find_programs_by_target",
2040
+ arguments: { target_query: "domain, URL, wildcard, package/repo name, or short target phrase", match_mode: "smart" }
2041
+ },
2042
+ {
2043
+ user_goal: "fresh or recently changed scope",
2044
+ first_tool: "get_recent_changes",
2045
+ then: "Use get_latest_added_targets when only newly added targets are needed."
2046
+ },
2047
+ {
2048
+ user_goal: "repository changes or repository polling",
2049
+ first_tool: "get_repository_changes",
2050
+ arguments: { include_targets: false, max_cursor_pages: 2 }
2051
+ },
2052
+ {
2053
+ user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
2054
+ first_tool: "get_repository_changes",
2055
+ arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
2056
+ parallel_with: {
2057
+ tool: "find_programs",
2058
+ arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
2059
+ },
2060
+ then: "Intersect event program ids with ranked program ids. Treat targets as event-level associations when target rows omit program_id, keep strong target scores/labels, and discard non-requested associated platforms."
2061
+ },
2062
+ {
2063
+ user_goal: "export targets",
2064
+ first_tool: "export_targets",
2065
+ then: "Use the returned bbradar://exports resource_uri for the full local export payload."
2066
+ },
2067
+ {
2068
+ user_goal: "unknown filter values",
2069
+ first_tool: "list_filters",
2070
+ then: "Reuse observed platforms, tags, target types, and language tags in find_programs."
2071
+ }
2072
+ ];
2073
+ return {
2074
+ topic,
2075
+ tool_profile: "core",
2076
+ rule_zero: "BBRadar is passive intelligence only. Never scan or contact targets.",
2077
+ defaults: {
2078
+ output_mode: "compact",
2079
+ target_list_mode: "identifiers",
2080
+ target_match_mode: "smart",
2081
+ include_out_of_scope: false,
2082
+ include_ineligible: false,
2083
+ require_bounty: true
2084
+ },
2085
+ primary_entry_points: [
2086
+ "find_programs",
2087
+ "run_program_name_action",
2088
+ "get_program_eligible_targets_by_name",
2089
+ "find_programs_by_target",
2090
+ "get_bbradar_guide"
2091
+ ],
2092
+ decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
2093
+ (topic === "start" || guideEntryMatchesTopic(topic, entry))),
2094
+ available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
2095
+ };
2096
+ }
2097
+ function toolAvailableForContract(client, toolName) {
2098
+ const endpoint = TOOL_ENDPOINT_REQUIREMENTS[toolName];
2099
+ return endpoint === undefined || client.supportsEndpoint(endpoint);
2100
+ }
1747
2101
  function guideEntryMatchesTopic(topic, entry) {
1748
2102
  const goal = typeof entry.user_goal === "string" ? entry.user_goal : "";
1749
2103
  const firstTool = typeof entry.first_tool === "string" ? entry.first_tool : "";
@@ -1770,15 +2124,52 @@ function guideEntryMatchesTopic(topic, entry) {
1770
2124
  }
1771
2125
  return true;
1772
2126
  }
1773
- function registerResources(server, client, config, exportStore) {
2127
+ function registerResources(server, client, config, exportStore, continuationStore) {
2128
+ server.registerResource("continuation", new ResourceTemplate("bbradar://continuations{?continuation_id,offset,limit}", { list: undefined }), {
2129
+ title: "BBRadar Result Continuation",
2130
+ description: "Bounded, short-lived in-memory continuation page for a large tool result.",
2131
+ mimeType: "application/json"
2132
+ }, async (uri) => {
2133
+ const continuationId = uri.searchParams.get("continuation_id") ?? "";
2134
+ pruneExpiredContinuations(continuationStore);
2135
+ const stored = continuationStore.get(continuationId);
2136
+ if (!stored) {
2137
+ return jsonResource(uri.toString(), {
2138
+ request_id: randomUUID(),
2139
+ error: { message: "Continuation not found or expired from local memory." }
2140
+ });
2141
+ }
2142
+ const offset = boundedResourceInteger(uri.searchParams.get("offset"), 0, 0, stored.items.length);
2143
+ const limit = boundedResourceInteger(uri.searchParams.get("limit"), config.continuationInlineLimit ?? 50, 1, 100);
2144
+ const items = stored.items.slice(offset, offset + limit);
2145
+ const nextOffset = offset + items.length;
2146
+ const hasMore = nextOffset < stored.items.length;
2147
+ return jsonResource(uri.toString(), {
2148
+ request_id: randomUUID(),
2149
+ continuation_id: continuationId,
2150
+ label: stored.label,
2151
+ items,
2152
+ meta: {
2153
+ offset,
2154
+ limit,
2155
+ returned: items.length,
2156
+ total: stored.items.length,
2157
+ has_more: hasMore,
2158
+ next_resource_uri: hasMore
2159
+ ? continuationResourceUri(continuationId, nextOffset, limit)
2160
+ : undefined
2161
+ }
2162
+ });
2163
+ });
1774
2164
  server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
1775
2165
  title: "BBRadar Target Export",
1776
- description: "In-memory local target export payload by export_id query parameter.",
2166
+ description: "Bounded, short-lived in-memory target export payload by export_id query parameter.",
1777
2167
  mimeType: "application/json"
1778
2168
  }, async (uri) => {
1779
2169
  const exportId = uri.searchParams.get("export_id") ?? "";
1780
- const payload = exportStore.get(exportId);
1781
- if (!payload) {
2170
+ pruneExpiredExports(exportStore);
2171
+ const storedExport = exportStore.get(exportId);
2172
+ if (!storedExport) {
1782
2173
  return jsonResource(uri.toString(), {
1783
2174
  request_id: randomUUID(),
1784
2175
  error: {
@@ -1789,7 +2180,7 @@ function registerResources(server, client, config, exportStore) {
1789
2180
  return jsonResource(uri.toString(), {
1790
2181
  request_id: randomUUID(),
1791
2182
  export_id: exportId,
1792
- export: payload
2183
+ export: storedExport.payload
1793
2184
  });
1794
2185
  });
1795
2186
  server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
@@ -3178,13 +3569,26 @@ async function findProgramsByTarget(client, config, input) {
3178
3569
  };
3179
3570
  const sourceRequests = [];
3180
3571
  const matches = [];
3572
+ const directHandleFirst = !queryLooksLikeTargetIdentifier(input.target_query) &&
3573
+ !/\s/.test(input.target_query.trim()) &&
3574
+ looksLikeDirectProgramHandleQuery(input.target_query);
3575
+ if (input.use_api_search && !directHandleFirst) {
3576
+ const directResult = await searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests);
3577
+ if (directResult) {
3578
+ return directResult;
3579
+ }
3580
+ }
3181
3581
  const directHandles = await collectDirectHandlePrograms(client, {
3182
3582
  query: input.target_query,
3183
3583
  platforms: input.platforms,
3184
3584
  budget,
3185
3585
  warnings,
3186
3586
  source: "target_query_program_id_guess",
3187
- maxProgramIds: queryLooksLikeTargetIdentifier(input.target_query) || looksLikeDirectProgramHandleQuery(input.target_query) ? MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS : 4,
3587
+ maxProgramIds: input.use_api_search && directHandleFirst
3588
+ ? 4
3589
+ : queryLooksLikeTargetIdentifier(input.target_query) || looksLikeDirectProgramHandleQuery(input.target_query)
3590
+ ? MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS
3591
+ : 4,
3188
3592
  maxHandleVariants: 1,
3189
3593
  stopOnFirstMatch: true
3190
3594
  });
@@ -3204,8 +3608,8 @@ async function findProgramsByTarget(client, config, input) {
3204
3608
  }
3205
3609
  });
3206
3610
  }
3207
- if (input.use_api_search) {
3208
- const directResult = await searchProgramsByTargetApi(client, config, input, warnings);
3611
+ if (input.use_api_search && directHandleFirst) {
3612
+ const directResult = await searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests);
3209
3613
  if (directResult) {
3210
3614
  return directResult;
3211
3615
  }
@@ -3310,13 +3714,22 @@ function programsByTargetResult(input, warnings, sourceRequests, matches, budget
3310
3714
  meta: options.meta
3311
3715
  });
3312
3716
  }
3313
- async function searchProgramsByTargetApi(client, config, input, warnings) {
3717
+ async function searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests) {
3718
+ if (!client.supportsEndpoint("target_search")) {
3719
+ warnings.push("The negotiated API contract does not expose direct target search; using sampled program target lists.");
3720
+ return undefined;
3721
+ }
3314
3722
  const searchQueries = targetApiSearchQueries(input.target_query);
3315
3723
  const grouped = new Map();
3316
- const sourceRequests = [];
3317
3724
  let rawMatchesScanned = 0;
3725
+ let searchQueriesChecked = 0;
3318
3726
  let lastMeta;
3319
3727
  for (const searchQuery of searchQueries) {
3728
+ if (!tryConsumeBudget(budget)) {
3729
+ addUniqueWarning(warnings, "Upstream request budget was exhausted before all target search variants could be checked.");
3730
+ break;
3731
+ }
3732
+ searchQueriesChecked += 1;
3320
3733
  let api;
3321
3734
  try {
3322
3735
  api = await client.searchTargets({
@@ -3327,10 +3740,12 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
3327
3740
  include_out_of_scope: input.include_out_of_scope,
3328
3741
  include_ineligible: input.include_ineligible,
3329
3742
  page: 1,
3330
- page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
3743
+ page_size: Math.min(client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM), Math.max(input.max_results * input.max_targets_per_program, input.max_results))
3331
3744
  });
3745
+ refundBudgetIfNoUpstreamRequest(budget, api);
3332
3746
  }
3333
3747
  catch (error) {
3748
+ refundBudgetIfNoUpstreamError(budget, error);
3334
3749
  if (error instanceof BBRadarApiError && (error.status === 404 || error.status === 405)) {
3335
3750
  warnings.push("Direct target search API is unavailable; fell back to sampled program target lists.");
3336
3751
  return undefined;
@@ -3371,15 +3786,93 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
3371
3786
  if (!targetMatchesQuery(target, input.target_query, input.match_mode)) {
3372
3787
  continue;
3373
3788
  }
3374
- const existing = grouped.get(programId) ?? { program: addProgramResourceLinks({ ...program, id: programId }), targets: [] };
3375
- existing.targets.push(target);
3789
+ const existing = grouped.get(programId) ?? {
3790
+ program: addProgramResourceLinks({ ...program, id: programId }),
3791
+ targets: [],
3792
+ targetKeys: new Set()
3793
+ };
3794
+ const targetKey = stableStringify({
3795
+ identifier: targetIdentifier(target),
3796
+ target_type: stringField(target, "target_type")
3797
+ });
3798
+ if (!existing.targetKeys.has(targetKey)) {
3799
+ existing.targetKeys.add(targetKey);
3800
+ existing.targets.push(target);
3801
+ }
3376
3802
  grouped.set(programId, existing);
3377
3803
  }
3378
3804
  }
3379
- if (grouped.size >= input.max_results) {
3805
+ if (grouped.size >= input.max_results ||
3806
+ (grouped.size > 0 && input.match_mode === "smart")) {
3380
3807
  break;
3381
3808
  }
3382
3809
  }
3810
+ if (grouped.size > 0 && input.match_mode === "smart") {
3811
+ await mapWithConcurrency([...grouped.entries()].slice(0, input.max_results), TARGET_SAMPLE_CONCURRENCY, async ([programId, entry]) => {
3812
+ if (!tryConsumeBudget(budget)) {
3813
+ addUniqueWarning(warnings, "Upstream request budget was exhausted before matched program target lists could be hydrated.");
3814
+ return;
3815
+ }
3816
+ try {
3817
+ const targetsApi = await client.getProgramTargets(programId);
3818
+ refundBudgetIfNoUpstreamRequest(budget, targetsApi);
3819
+ sourceRequests.push({
3820
+ source: "program_targets",
3821
+ program_id: programId,
3822
+ request_id: targetsApi.requestId,
3823
+ upstream_request_id: targetsApi.upstreamRequestId,
3824
+ ...apiSourceMetadata(targetsApi)
3825
+ });
3826
+ const targetsData = readObject(targetsApi.data);
3827
+ const hydratedTargets = readArray(targetsData?.targets).map(sanitizeTarget);
3828
+ const hydratedTargetOrder = new Map(hydratedTargets.map((target, index) => [
3829
+ stableStringify({
3830
+ identifier: targetIdentifier(target),
3831
+ target_type: stringField(target, "target_type")
3832
+ }),
3833
+ index
3834
+ ]));
3835
+ for (const target of hydratedTargets) {
3836
+ if (!targetIdentifier(target) ||
3837
+ !targetHasAllowedScope(target, {
3838
+ include_out_of_scope: input.include_out_of_scope,
3839
+ include_ineligible: input.include_ineligible,
3840
+ strict_scope_filter: false
3841
+ }) ||
3842
+ !targetMatchesQuery(target, input.target_query, input.match_mode)) {
3843
+ continue;
3844
+ }
3845
+ const targetKey = stableStringify({
3846
+ identifier: targetIdentifier(target),
3847
+ target_type: stringField(target, "target_type")
3848
+ });
3849
+ if (!entry.targetKeys.has(targetKey)) {
3850
+ entry.targetKeys.add(targetKey);
3851
+ entry.targets.push(target);
3852
+ }
3853
+ }
3854
+ entry.targets.sort((left, right) => {
3855
+ const leftKey = stableStringify({
3856
+ identifier: targetIdentifier(left),
3857
+ target_type: stringField(left, "target_type")
3858
+ });
3859
+ const rightKey = stableStringify({
3860
+ identifier: targetIdentifier(right),
3861
+ target_type: stringField(right, "target_type")
3862
+ });
3863
+ return ((hydratedTargetOrder.get(leftKey) ?? Number.MAX_SAFE_INTEGER) -
3864
+ (hydratedTargetOrder.get(rightKey) ?? Number.MAX_SAFE_INTEGER));
3865
+ });
3866
+ }
3867
+ catch (error) {
3868
+ refundBudgetIfNoUpstreamError(budget, error);
3869
+ if (!isProgramNotFoundError(error)) {
3870
+ throw error;
3871
+ }
3872
+ addUniqueWarning(warnings, `Could not hydrate target matches for ${programId}; returned direct target-search matches only.`);
3873
+ }
3874
+ });
3875
+ }
3383
3876
  const matches = [...grouped.values()].slice(0, input.max_results).map((entry) => stripUndefined({
3384
3877
  program: formatProgram(entry.program, "compact"),
3385
3878
  match_count: entry.targets.length,
@@ -3407,6 +3900,14 @@ async function searchProgramsByTargetApi(client, config, input, warnings) {
3407
3900
  },
3408
3901
  matches,
3409
3902
  programs: matches.map((match) => readObject(match.program)).filter((program) => program !== undefined),
3903
+ upstream_budget: {
3904
+ used: budget.initial - budget.remaining,
3905
+ remaining: budget.remaining
3906
+ },
3907
+ ranking_scope: {
3908
+ mode: "target_search_api",
3909
+ possibly_incomplete: grouped.size < input.max_results && searchQueriesChecked < searchQueries.length
3910
+ },
3410
3911
  meta: stripUndefined({
3411
3912
  ...(lastMeta ?? {}),
3412
3913
  returned: matches.length,
@@ -3503,7 +4004,8 @@ async function listFilters(client, input) {
3503
4004
  }
3504
4005
  }
3505
4006
  }
3506
- catch {
4007
+ catch (error) {
4008
+ refundBudgetIfNoUpstreamError(budget, error);
3507
4009
  // Facets are best-effort; list_filters should still return observed program filters.
3508
4010
  }
3509
4011
  if (input.include_target_facets && input.target_sample_programs > 0) {
@@ -3639,11 +4141,15 @@ async function comparePrograms(client, config, input) {
3639
4141
  });
3640
4142
  }
3641
4143
  async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
3642
- const programApi = await client.getProgram(programId);
4144
+ const [programApi, changes] = await Promise.all([
4145
+ client.getProgram(programId),
4146
+ recentChangesLimit > 0
4147
+ ? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
4148
+ : Promise.resolve(undefined)
4149
+ ]);
3643
4150
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
3644
4151
  const scopeSummary = readObject(program.scope_summary);
3645
4152
  const targetActivity = readObject(scopeSummary?.target_activity);
3646
- const changes = recentChangesLimit > 0 ? await fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true) : undefined;
3647
4153
  return {
3648
4154
  request_id: randomUUID(),
3649
4155
  source_requests: [
@@ -3670,12 +4176,14 @@ async function getLatestAddedTargets(client, config, input) {
3670
4176
  include_removed: false,
3671
4177
  include_ineligible: input.include_ineligible,
3672
4178
  include_out_of_scope: input.include_out_of_scope,
4179
+ platforms: input.platforms,
3673
4180
  tags: [input.target_type],
3674
4181
  page: 1,
3675
4182
  page_size: input.recent_changes_page_size
3676
4183
  });
3677
4184
  const recentData = readObject(recentApi.data);
3678
4185
  const changes = readArray(recentData?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
4186
+ const platformFilters = new Set(input.platforms.map(normalizeTag));
3679
4187
  const matchingChanges = changes
3680
4188
  .filter((change) => {
3681
4189
  const target = readObject(change.target);
@@ -3684,6 +4192,7 @@ async function getLatestAddedTargets(client, config, input) {
3684
4192
  programId !== undefined &&
3685
4193
  target !== undefined &&
3686
4194
  targetMatchesRequestedType(target, input.target_type) &&
4195
+ latestAddedProgramMatches(change, input, platformFilters) &&
3687
4196
  targetHasAllowedScope(target, {
3688
4197
  include_out_of_scope: input.include_out_of_scope,
3689
4198
  include_ineligible: input.include_ineligible,
@@ -3691,6 +4200,11 @@ async function getLatestAddedTargets(client, config, input) {
3691
4200
  }));
3692
4201
  })
3693
4202
  .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
4203
+ const limitedMatches = matchingChanges.slice(0, input.max_results);
4204
+ const matches = limitedMatches.map((change, index) => ({
4205
+ rank: index + 1,
4206
+ ...formatChange(change, "compact")
4207
+ }));
3694
4208
  const latest = matchingChanges[0];
3695
4209
  const sourceRequests = [
3696
4210
  {
@@ -3705,9 +4219,12 @@ async function getLatestAddedTargets(client, config, input) {
3705
4219
  request_id: randomUUID(),
3706
4220
  source_requests: sourceRequests,
3707
4221
  query: latestAddedTargetsQuery(input),
4222
+ matches: [],
3708
4223
  new_targets: [],
3709
4224
  meta: {
3710
4225
  recent_changes_scanned: changes.length,
4226
+ matching_changes_scanned: 0,
4227
+ matches_returned: 0,
3711
4228
  no_match: true
3712
4229
  }
3713
4230
  };
@@ -3750,11 +4267,14 @@ async function getLatestAddedTargets(client, config, input) {
3750
4267
  query: latestAddedTargetsQuery(input),
3751
4268
  latest_added_at: latestAddedAt,
3752
4269
  program,
4270
+ matches,
3753
4271
  new_targets: formatTargetList(latestNewTargets, input.full_target_list_mode),
3754
4272
  full_targets: input.include_full_target_list ? formatTargetList(limitedFullTargets, input.full_target_list_mode) : undefined,
3755
4273
  meta: stripUndefined({
3756
4274
  recent_changes_scanned: changes.length,
3757
4275
  matching_changes_scanned: matchingChanges.length,
4276
+ matches_returned: matches.length,
4277
+ matches_has_more: matches.length < matchingChanges.length,
3758
4278
  new_targets_returned: formatTargetList(latestNewTargets, input.full_target_list_mode).length,
3759
4279
  full_targets_total_active: totalActiveTargets,
3760
4280
  full_targets_total_after_filters: totalAfterFilters,
@@ -3763,9 +4283,52 @@ async function getLatestAddedTargets(client, config, input) {
3763
4283
  })
3764
4284
  });
3765
4285
  }
4286
+ function latestAddedProgramMatches(change, input, platformFilters) {
4287
+ const rawProgram = readObject(change.program);
4288
+ if (!rawProgram) {
4289
+ return false;
4290
+ }
4291
+ const program = addProgramResourceLinks(rawProgram);
4292
+ const rewardRange = readObject(program.reward_range);
4293
+ const platform = normalizeTag(stringField(program, "platform") ?? "");
4294
+ const bountyMin = numberField(rewardRange, "min");
4295
+ const bountyMax = numberField(rewardRange, "max");
4296
+ const publicReportCount = numberField(program, "public_report_count");
4297
+ if (platformFilters.size > 0 && !platformFilters.has(platform)) {
4298
+ return false;
4299
+ }
4300
+ if (input.min_reward !== undefined) {
4301
+ const reward = input.reward_threshold_mode === "min_at_least" ? bountyMin : bountyMax;
4302
+ if ((reward ?? 0) < input.min_reward) {
4303
+ return false;
4304
+ }
4305
+ }
4306
+ if (input.require_known_report_count && publicReportCount === undefined) {
4307
+ return false;
4308
+ }
4309
+ if (input.max_public_report_count !== undefined) {
4310
+ if (publicReportCount === undefined) {
4311
+ return input.include_unknown_report_count;
4312
+ }
4313
+ if (publicReportCount > input.max_public_report_count) {
4314
+ return false;
4315
+ }
4316
+ }
4317
+ return true;
4318
+ }
3766
4319
  async function getProgramTargetsPayload(client, input, programId) {
3767
- const api = await client.getProgramTargets(programId);
4320
+ const api = await client.getProgramTargets(programId, {
4321
+ search: input.search,
4322
+ target_type: input.target_type,
4323
+ eligibility: input.include_ineligible ? "all" : "eligible",
4324
+ minTargetScore: input.min_target_score,
4325
+ targetOpportunityLabel: input.target_opportunity_label,
4326
+ competitionRisk: input.competition_risk,
4327
+ page: 1,
4328
+ page_size: client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM)
4329
+ });
3768
4330
  const data = readObject(api.data);
4331
+ const upstreamMeta = readObject(data?.meta);
3769
4332
  const rawTargets = readArray(data?.targets);
3770
4333
  const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
3771
4334
  const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
@@ -3776,10 +4339,13 @@ async function getProgramTargetsPayload(client, input, programId) {
3776
4339
  offset: input.offset,
3777
4340
  limit: input.limit,
3778
4341
  output_mode: input.output_mode,
3779
- total_active_targets: rawTargets.length,
4342
+ total_active_targets: readNumber(upstreamMeta?.total) ?? rawTargets.length,
3780
4343
  total_after_filters: sanitizedTargets.length,
3781
4344
  returned: targets.length,
3782
- has_more: input.offset + input.limit < sanitizedTargets.length,
4345
+ has_more: input.offset + input.limit < sanitizedTargets.length || readBoolean(upstreamMeta?.has_more) === true,
4346
+ upstream_has_more: readBoolean(upstreamMeta?.has_more),
4347
+ possibly_incomplete: readBoolean(upstreamMeta?.has_more) === true,
4348
+ data_capabilities: readObject(upstreamMeta?.data_capabilities),
3783
4349
  target_source: "program_targets"
3784
4350
  }
3785
4351
  });
@@ -4319,9 +4885,11 @@ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback,
4319
4885
  });
4320
4886
  }
4321
4887
  async function getProgramScopeDelta(client, config, input) {
4322
- const programApi = await client.getProgram(input.program_id);
4888
+ const [programApi, changesPayload] = await Promise.all([
4889
+ client.getProgram(input.program_id),
4890
+ fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets)
4891
+ ]);
4323
4892
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
4324
- const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope, input.max_targets);
4325
4893
  const filteredChanges = filterProgramScopeDeltaChanges(changesPayload.changes, input);
4326
4894
  const limitedChanges = filteredChanges.slice(0, input.max_targets);
4327
4895
  return stripUndefined({
@@ -4776,9 +5344,11 @@ async function getProgramBrief(client, config, input) {
4776
5344
  });
4777
5345
  }
4778
5346
  async function getProgramDelta(client, config, input) {
4779
- const programApi = await client.getProgram(input.program_id);
5347
+ const [programApi, changes] = await Promise.all([
5348
+ client.getProgram(input.program_id),
5349
+ fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed, input.include_ineligible, input.include_out_of_scope)
5350
+ ]);
4780
5351
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
4781
- const changes = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed, input.include_ineligible, input.include_out_of_scope);
4782
5352
  return {
4783
5353
  request_id: randomUUID(),
4784
5354
  source_requests: [
@@ -4860,6 +5430,79 @@ function programTargetsResourceUri(programId) {
4860
5430
  function exportResourceUri(exportId) {
4861
5431
  return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
4862
5432
  }
5433
+ function continuationResourceUri(continuationId, offset, limit) {
5434
+ return `bbradar://continuations?continuation_id=${encodeURIComponent(continuationId)}&offset=${offset}&limit=${limit}`;
5435
+ }
5436
+ function applyCollectionContinuation(payload, field, label, store, config) {
5437
+ const items = Array.isArray(payload[field]) ? payload[field] : undefined;
5438
+ const inlineLimit = Math.min(100, Math.max(1, config.continuationInlineLimit ?? 50));
5439
+ if (!items || items.length <= inlineLimit) {
5440
+ return payload;
5441
+ }
5442
+ const continuationId = randomUUID();
5443
+ const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
5444
+ const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
5445
+ if (!stored) {
5446
+ return {
5447
+ ...payload,
5448
+ [field]: items.slice(0, inlineLimit),
5449
+ warnings: uniqueStrings([
5450
+ ...readArray(payload.warnings).filter((entry) => typeof entry === "string"),
5451
+ "The complete result exceeded the local continuation-store byte limit; only the inline prefix is available."
5452
+ ])
5453
+ };
5454
+ }
5455
+ return {
5456
+ ...payload,
5457
+ [field]: items.slice(0, inlineLimit),
5458
+ continuation: {
5459
+ continuation_id: continuationId,
5460
+ resource_uri: continuationResourceUri(continuationId, inlineLimit, inlineLimit),
5461
+ collection: field,
5462
+ total: items.length,
5463
+ inline_returned: inlineLimit,
5464
+ remaining: items.length - inlineLimit,
5465
+ expires_at: new Date(expiresAt).toISOString()
5466
+ }
5467
+ };
5468
+ }
5469
+ function rememberContinuation(store, id, label, items, expiresAt) {
5470
+ const byteLength = Buffer.byteLength(JSON.stringify(items), "utf8");
5471
+ pruneExpiredContinuations(store);
5472
+ if (byteLength > MAX_LOCAL_EXPORT_BYTES) {
5473
+ return false;
5474
+ }
5475
+ while (store.size >= MAX_LOCAL_EXPORT_RESOURCES ||
5476
+ retainedContinuationBytes(store) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
5477
+ const oldest = store.keys().next().value;
5478
+ if (oldest === undefined) {
5479
+ break;
5480
+ }
5481
+ store.delete(oldest);
5482
+ }
5483
+ store.set(id, { items, label, byteLength, expiresAt });
5484
+ return true;
5485
+ }
5486
+ function pruneExpiredContinuations(store, now = Date.now()) {
5487
+ for (const [id, value] of store) {
5488
+ if (value.expiresAt <= now) {
5489
+ store.delete(id);
5490
+ }
5491
+ }
5492
+ }
5493
+ function retainedContinuationBytes(store) {
5494
+ let total = 0;
5495
+ for (const value of store.values()) {
5496
+ total += value.byteLength;
5497
+ }
5498
+ return total;
5499
+ }
5500
+ function boundedResourceInteger(raw, fallback, minimum, maximum) {
5501
+ const parsed = raw === null ? fallback : Number.parseInt(raw, 10);
5502
+ return Number.isFinite(parsed)
5503
+ ? Math.min(maximum, Math.max(minimum, parsed))
5504
+ : fallback;
5505
+ }
4863
5506
  function jsonResource(uri, payload) {
4864
5507
  return {
4865
5508
  contents: [
@@ -4872,41 +5515,86 @@ function jsonResource(uri, payload) {
4872
5515
  };
4873
5516
  }
4874
5517
  function rememberExport(exportStore, exportId, payload) {
5518
+ const now = Date.now();
5519
+ const byteLength = Buffer.byteLength(JSON.stringify(payload), "utf8");
5520
+ pruneExpiredExports(exportStore, now);
4875
5521
  if (exportStore.has(exportId)) {
4876
5522
  exportStore.delete(exportId);
4877
5523
  }
4878
- while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES) {
5524
+ while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES ||
5525
+ retainedExportBytes(exportStore) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
4879
5526
  const oldestKey = exportStore.keys().next().value;
4880
5527
  if (oldestKey === undefined) {
4881
5528
  break;
4882
5529
  }
4883
5530
  exportStore.delete(oldestKey);
4884
5531
  }
4885
- exportStore.set(exportId, payload);
5532
+ exportStore.set(exportId, {
5533
+ payload,
5534
+ byteLength,
5535
+ expiresAt: now + LOCAL_EXPORT_TTL_MS
5536
+ });
5537
+ }
5538
+ function pruneExpiredExports(exportStore, now = Date.now()) {
5539
+ for (const [exportId, storedExport] of exportStore) {
5540
+ if (storedExport.expiresAt <= now) {
5541
+ exportStore.delete(exportId);
5542
+ }
5543
+ }
5544
+ }
5545
+ function retainedExportBytes(exportStore) {
5546
+ let total = 0;
5547
+ for (const storedExport of exportStore.values()) {
5548
+ total += storedExport.byteLength;
5549
+ }
5550
+ return total;
4886
5551
  }
4887
5552
  function previewExportPayload(payload) {
5553
+ if (typeof payload === "string") {
5554
+ return previewExportText(payload);
5555
+ }
4888
5556
  if (Array.isArray(payload)) {
4889
5557
  return payload.slice(0, EXPORT_PREVIEW_LIMIT);
4890
5558
  }
4891
- if (readObject(payload) && Array.isArray(payload.targets)) {
5559
+ const object = readObject(payload);
5560
+ if (object && typeof object.content === "string") {
5561
+ return {
5562
+ ...object,
5563
+ content: previewExportText(object.content)
5564
+ };
5565
+ }
5566
+ if (object && Array.isArray(object.targets)) {
4892
5567
  return {
4893
- ...payload,
4894
- targets: payload.targets.slice(0, EXPORT_PREVIEW_LIMIT)
5568
+ ...object,
5569
+ targets: object.targets.slice(0, EXPORT_PREVIEW_LIMIT)
4895
5570
  };
4896
5571
  }
4897
5572
  return payload;
4898
5573
  }
4899
5574
  function previewExportCount(payload) {
5575
+ if (typeof payload === "string") {
5576
+ return Math.min(Math.max(0, payload.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
5577
+ }
4900
5578
  if (Array.isArray(payload)) {
4901
5579
  return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
4902
5580
  }
4903
5581
  const object = readObject(payload);
5582
+ if (typeof object?.content === "string") {
5583
+ return Math.min(Math.max(0, object.content.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
5584
+ }
4904
5585
  const targets = object?.targets;
4905
5586
  if (Array.isArray(targets)) {
4906
5587
  return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
4907
5588
  }
4908
5589
  return payload === undefined || payload === null ? 0 : 1;
4909
5590
  }
5591
+ function previewExportText(payload) {
5592
+ const lines = payload.split(/\r?\n/);
5593
+ const preview = lines.slice(0, EXPORT_PREVIEW_LIMIT + 1).join("\n");
5594
+ return lines.length > EXPORT_PREVIEW_LIMIT + 1
5595
+ ? `${preview}\n[preview truncated; read the export resource for the complete payload]`
5596
+ : preview;
5597
+ }
4910
5598
  function collectResourceLinks(payload) {
4911
5599
  const links = new Map();
4912
5600
  for (const program of extractProgramsFromPayload(payload)) {
@@ -4948,6 +5636,19 @@ function collectResourceLinks(payload) {
4948
5636
  mimeType: "application/json"
4949
5637
  });
4950
5638
  }
5639
+ const continuation = readObject(payload.continuation);
5640
+ const continuationUri = stringField(continuation, "resource_uri");
5641
+ const continuationId = stringField(continuation, "continuation_id");
5642
+ if (continuationUri && continuationId) {
5643
+ links.set(continuationUri, {
5644
+ type: "resource_link",
5645
+ uri: continuationUri,
5646
+ name: `${continuationId} continuation`,
5647
+ title: "Result continuation",
5648
+ description: "Read the next bounded page of this local in-memory result.",
5649
+ mimeType: "application/json"
5650
+ });
5651
+ }
4951
5652
  return [...links.values()];
4952
5653
  }
4953
5654
  function extractProgramsFromPayload(payload) {
@@ -4997,9 +5698,18 @@ function compactProgram(program) {
4997
5698
  handle: stringField(program, "handle"),
4998
5699
  name: stringField(program, "name"),
4999
5700
  platform: stringField(program, "platform"),
5701
+ catalog: stringField(program, "catalog"),
5702
+ program_type: stringField(program, "program_type"),
5000
5703
  platform_url: stringField(program, "platform_url"),
5001
5704
  bbradar_url: stringField(program, "bbradar_url"),
5705
+ policy_url: stringField(program, "policy_url"),
5706
+ reporting_url: stringField(program, "reporting_url"),
5707
+ reward_status: stringField(program, "reward_status"),
5708
+ data_capabilities: readObject(program.data_capabilities),
5002
5709
  opportunity_tag: readObject(program.opportunity_tag),
5710
+ competition_risk: stringField(program, "competition_risk"),
5711
+ competition_risk_reason: stringField(program, "competition_risk_reason"),
5712
+ submission_cost: readObject(program.submission_cost),
5003
5713
  reward_range: readObject(program.reward_range),
5004
5714
  public_report_count: readNumber(program.public_report_count),
5005
5715
  public_report_count_label: stringField(program, "public_report_count_label"),
@@ -5049,6 +5759,30 @@ function formatChange(change, mode) {
5049
5759
  target: target ? compactTarget(target) : undefined
5050
5760
  });
5051
5761
  }
5762
+ function formatRepositoryChange(change, mode) {
5763
+ if (mode === "full") {
5764
+ return change;
5765
+ }
5766
+ const repository = readObject(change.repository);
5767
+ return stripUndefined({
5768
+ id: change.id,
5769
+ event_type: stringField(change, "event_type"),
5770
+ ref_name: stringField(change, "ref_name"),
5771
+ observed_at: stringField(change, "observed_at"),
5772
+ repository: repository
5773
+ ? stripUndefined({
5774
+ provider: stringField(repository, "provider"),
5775
+ full_name: stringField(repository, "full_name"),
5776
+ url: stringField(repository, "url")
5777
+ })
5778
+ : undefined,
5779
+ changes_url: stringField(change, "changes_url"),
5780
+ programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
5781
+ targets: Array.isArray(change.targets)
5782
+ ? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
5783
+ : undefined
5784
+ });
5785
+ }
5052
5786
  function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
5053
5787
  const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
5054
5788
  const groups = {
@@ -5300,6 +6034,14 @@ function candidateLooksContestLike(candidate) {
5300
6034
  return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
5301
6035
  }
5302
6036
  function candidateHasKnownNoBounty(candidate) {
6037
+ const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
6038
+ const rewardStatus = normalizeTag(stringField(candidate.program, "reward_status") ?? "");
6039
+ if (programType === "vdp" || rewardStatus === "no_reward" || rewardStatus === "no reward") {
6040
+ return true;
6041
+ }
6042
+ if (programType === "paid" || rewardStatus === "paid") {
6043
+ return false;
6044
+ }
5303
6045
  if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
5304
6046
  return false;
5305
6047
  }
@@ -5372,6 +6114,9 @@ function hasHighConfidenceProgramResolutionMatch(programs, webBaseUrl, query) {
5372
6114
  const threshold = looksLikeDirectProgramHandleQuery(query) ? 95 : 85;
5373
6115
  return programs.some((program) => programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score >= threshold);
5374
6116
  }
6117
+ function hasViableProgramResolutionMatch(programs, scorePrograms) {
6118
+ return scorePrograms ? scorePrograms(programs) >= 80 : programs.length > 0;
6119
+ }
5375
6120
  function bestProgramResolutionScore(programs, webBaseUrl, query) {
5376
6121
  return programs.reduce((best, program) => Math.max(best, programResolutionMatch(toProgramCandidate(program, webBaseUrl), query).score), 0);
5377
6122
  }
@@ -5485,6 +6230,16 @@ function normalizeFindProgramsInput(input) {
5485
6230
  const warnings = [];
5486
6231
  const normalized = { ...input };
5487
6232
  normalized.vdp_mode ??= "exclude";
6233
+ normalized.exclude_platforms = uniqueStrings(normalized.exclude_platforms ?? []);
6234
+ if (normalized.web3 && normalized.exclude_web3) {
6235
+ normalized.web3 = false;
6236
+ warnings.push("exclude_web3=true took precedence over the contradictory web3=true filter.");
6237
+ }
6238
+ const includedPlatforms = new Set(normalized.platforms.map(normalizeTag));
6239
+ const overlappingPlatforms = normalized.exclude_platforms.filter((platform) => includedPlatforms.has(normalizeTag(platform)));
6240
+ if (overlappingPlatforms.length > 0) {
6241
+ warnings.push(`Excluded platform filters take precedence over included values: ${overlappingPlatforms.join(", ")}.`);
6242
+ }
5488
6243
  normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
5489
6244
  normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
5490
6245
  normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
@@ -5511,7 +6266,13 @@ async function findPrograms(client, config, input) {
5511
6266
  initial: input.upstream_request_budget,
5512
6267
  remaining: input.upstream_request_budget
5513
6268
  };
5514
- const upstreamTags = uniqueStrings([...input.tags, ...input.scope_tags, ...input.target_types, ...input.language_tags]);
6269
+ const upstreamTags = uniqueStrings([
6270
+ ...input.tags,
6271
+ ...input.scope_tags,
6272
+ ...input.target_types,
6273
+ ...input.language_tags,
6274
+ ...(input.min_wildcards > 0 ? ["wildcard"] : [])
6275
+ ]);
5515
6276
  const collected = await collectPrograms(client, {
5516
6277
  platforms: input.platforms,
5517
6278
  tags: upstreamTags,
@@ -5545,8 +6306,10 @@ async function findPrograms(client, config, input) {
5545
6306
  warnings: warnings.length > 0 ? warnings : undefined,
5546
6307
  filters: stripUndefined({
5547
6308
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
6309
+ exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
5548
6310
  tags: upstreamTags.length > 0 ? upstreamTags : undefined,
5549
6311
  web3: input.web3 ? true : undefined,
6312
+ exclude_web3: input.exclude_web3 ? true : undefined,
5550
6313
  opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
5551
6314
  updated_since: input.updated_since,
5552
6315
  min_bounty_min: input.min_bounty_min,
@@ -5623,8 +6386,33 @@ async function collectProgramsForResolution(client, options) {
5623
6386
  const collections = [];
5624
6387
  const queryCollections = [];
5625
6388
  const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
6389
+ const fastQueries = queries.slice(0, FAST_RESOLVE_SEARCH_QUERIES);
6390
+ const remainingQueries = queries.slice(FAST_RESOLVE_SEARCH_QUERIES);
5626
6391
  const primaryDirectPlatforms = options.platforms.length > 0 ? options.platforms : PRIMARY_PROGRAM_ID_PLATFORM_GUESSES;
5627
6392
  const primaryDirectProgramIds = new Set(directProgramIdCandidates(options.query, primaryDirectPlatforms, { maxHandleVariants: 1 }));
6393
+ for (const search of fastQueries) {
6394
+ if (options.budget.remaining <= 0) {
6395
+ addUniqueWarning(options.warnings, "Program name search stopped at the request budget; pass a higher upstream_request_budget for a deeper resolution attempt.");
6396
+ break;
6397
+ }
6398
+ const collection = await collectPrograms(client, {
6399
+ search,
6400
+ platforms: options.platforms,
6401
+ tags: options.tags,
6402
+ updated_since: undefined,
6403
+ opportunity_levels: options.opportunity_levels,
6404
+ max_pages: 1,
6405
+ budget: options.budget,
6406
+ warnings: options.warnings
6407
+ });
6408
+ collections.push(collection);
6409
+ queryCollections.push({ search, collection });
6410
+ const merged = mergeProgramCollections(collections);
6411
+ if (options.isHighConfidenceMatch?.(merged.programs) ||
6412
+ hasViableProgramResolutionMatch(merged.programs, options.scorePrograms)) {
6413
+ return merged;
6414
+ }
6415
+ }
5628
6416
  if (primaryDirectProgramIds.size > 0 && options.budget.remaining > 0) {
5629
6417
  const primaryDirectHandles = await collectDirectHandlePrograms(client, {
5630
6418
  query: options.query,
@@ -5644,7 +6432,7 @@ async function collectProgramsForResolution(client, options) {
5644
6432
  return mergeProgramCollections(collections);
5645
6433
  }
5646
6434
  }
5647
- for (const search of queries) {
6435
+ for (const search of remainingQueries) {
5648
6436
  if (options.budget.remaining <= 0) {
5649
6437
  addUniqueWarning(options.warnings, "Program name search stopped at the request budget; pass a higher upstream_request_budget for a deeper resolution attempt.");
5650
6438
  break;
@@ -5665,7 +6453,9 @@ async function collectProgramsForResolution(client, options) {
5665
6453
  return mergeProgramCollections(collections);
5666
6454
  }
5667
6455
  }
5668
- if (options.budget.remaining > 0) {
6456
+ const searchedCollection = collections.length > 0 ? mergeProgramCollections(collections) : undefined;
6457
+ if (options.budget.remaining > 0 &&
6458
+ (!searchedCollection || !hasViableProgramResolutionMatch(searchedCollection.programs, options.scorePrograms))) {
5669
6459
  const directHandles = await collectDirectHandlePrograms(client, {
5670
6460
  query: options.query,
5671
6461
  platforms: options.platforms,
@@ -5752,9 +6542,14 @@ async function collectDirectHandlePrograms(client, options) {
5752
6542
  const sourceRequests = [];
5753
6543
  let requestsScanned = 0;
5754
6544
  for (const programId of programIds) {
6545
+ if (!tryConsumeBudget(options.budget)) {
6546
+ addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all direct program handle guesses could be checked.");
6547
+ break;
6548
+ }
5755
6549
  requestsScanned += 1;
5756
6550
  try {
5757
6551
  const targetsApi = await client.getProgramTargets(programId);
6552
+ refundBudgetIfNoUpstreamRequest(options.budget, targetsApi);
5758
6553
  const targetsData = readObject(targetsApi.data);
5759
6554
  const hasTargetsArray = Array.isArray(targetsData?.targets);
5760
6555
  const targets = readArray(targetsData?.targets).map(sanitizeTarget);
@@ -5775,6 +6570,7 @@ async function collectDirectHandlePrograms(client, options) {
5775
6570
  }
5776
6571
  }
5777
6572
  catch (error) {
6573
+ refundBudgetIfNoUpstreamError(options.budget, error);
5778
6574
  if (error instanceof BBRadarApiError && error.status === 404) {
5779
6575
  sourceRequests.push({
5780
6576
  source: options.source,
@@ -5782,7 +6578,9 @@ async function collectDirectHandlePrograms(client, options) {
5782
6578
  program_id: programId,
5783
6579
  request_id: error.requestId,
5784
6580
  upstream_request_id: error.upstreamRequestId,
5785
- status: error.status
6581
+ status: error.status,
6582
+ cache_hit: error.cached,
6583
+ cache_expires_at: error.cacheExpiresAt
5786
6584
  });
5787
6585
  continue;
5788
6586
  }
@@ -6043,12 +6841,19 @@ function programCandidateMatches(candidate, filters) {
6043
6841
  if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
6044
6842
  return false;
6045
6843
  }
6844
+ if ((filters.exclude_platforms?.length ?? 0) > 0 &&
6845
+ filters.exclude_platforms?.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
6846
+ return false;
6847
+ }
6046
6848
  if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
6047
6849
  return false;
6048
6850
  }
6049
6851
  if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
6050
6852
  return false;
6051
6853
  }
6854
+ if (filters.exclude_web3 && candidateHasWeb3Surface(candidate)) {
6855
+ return false;
6856
+ }
6052
6857
  if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
6053
6858
  return false;
6054
6859
  }
@@ -6061,7 +6866,9 @@ function programCandidateMatches(candidate, filters) {
6061
6866
  if (candidate.wildcardCount < filters.min_wildcards) {
6062
6867
  return false;
6063
6868
  }
6064
- if (filters.require_bounty && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
6869
+ const explicitlyPaid = normalizeTag(stringField(candidate.program, "program_type") ?? "") === "paid" ||
6870
+ normalizeTag(stringField(candidate.program, "reward_status") ?? "") === "paid";
6871
+ if (filters.require_bounty && !explicitlyPaid && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
6065
6872
  return false;
6066
6873
  }
6067
6874
  if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
@@ -6195,7 +7002,8 @@ async function collectTargetSamples(client, candidates, input, budget) {
6195
7002
  .slice(0, input.target_sample_size);
6196
7003
  samples[programId] = targets;
6197
7004
  }
6198
- catch {
7005
+ catch (error) {
7006
+ refundBudgetIfNoUpstreamError(budget, error);
6199
7007
  samples[programId] = [];
6200
7008
  }
6201
7009
  });
@@ -6258,6 +7066,11 @@ function refundBudgetIfNoUpstreamRequest(budget, api, cost = 1) {
6258
7066
  budget.remaining = Math.min(budget.initial, budget.remaining + cost);
6259
7067
  }
6260
7068
  }
7069
+ function refundBudgetIfNoUpstreamError(budget, error, cost = 1) {
7070
+ if (error instanceof BBRadarApiError && (error.cached || error.coalesced)) {
7071
+ budget.remaining = Math.min(budget.initial, budget.remaining + cost);
7072
+ }
7073
+ }
6261
7074
  function addUniqueWarning(warnings, warning) {
6262
7075
  if (warnings && !warnings.includes(warning)) {
6263
7076
  warnings.push(warning);
@@ -6360,7 +7173,12 @@ function readStringArrayField(value, field) {
6360
7173
  const fieldValue = value[field];
6361
7174
  return Array.isArray(fieldValue) ? fieldValue.filter((entry) => typeof entry === "string") : [];
6362
7175
  }
6363
- function registerPrompts(server) {
7176
+ function registerPrompts(server, profile) {
7177
+ const discoveryToolInstructions = profile === "core"
7178
+ ? `- Start with find_programs and pass opportunity_levels from the requested value below unless the user asked for a broader search.
7179
+ - Express language, target type, reward, Web3, wildcard, VDP, and platform needs through find_programs filters.`
7180
+ : `- Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from the requested value below.
7181
+ - 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.`;
6364
7182
  server.registerPrompt("find_best_programs_to_hunt", {
6365
7183
  title: "Find Best Programs To Hunt",
6366
7184
  description: "Rank fresh BBRadar program data only. Do not invoke external bug bounty skills unless the user explicitly asks for methodology.",
@@ -6373,8 +7191,8 @@ function registerPrompts(server) {
6373
7191
  }, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Treat results as fresh API reads; do not describe them as cached.
6374
7192
 
6375
7193
  Find the best BBRadar program candidates.
6376
- - Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from this JSON array unless the user asked for a broader search: ${promptJson([args.opportunity_level ?? "elite"])}.
6377
- - 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.
7194
+ ${discoveryToolInstructions}
7195
+ - Use this opportunity level JSON array: ${promptJson([args.opportunity_level ?? "elite"])}.
6378
7196
  - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
6379
7197
  - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
6380
7198
  - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
@@ -6421,6 +7239,111 @@ The plan must:
6421
7239
  - Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
6422
7240
  - Include a short checklist of BBRadar follow-up tool calls and decision points.`));
6423
7241
  }
7242
+ function resolveFeedCursor(store, endpoint, filters, resume) {
7243
+ const normalizedFilters = stableStringify(normalizeFeedCursorFilters(filters));
7244
+ const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
7245
+ const explicitAfterId = readNumber(filters.after_id);
7246
+ if (explicitAfterId !== undefined) {
7247
+ return { endpoint, key, afterId: explicitAfterId, resumed: false };
7248
+ }
7249
+ if (!resume) {
7250
+ return { endpoint, key, afterId: undefined, resumed: false };
7251
+ }
7252
+ const stored = store.get(key);
7253
+ if (stored === undefined) {
7254
+ throw new Error(`No in-memory ${endpoint} cursor exists for this filter set. Provide after_id once to seed it.`);
7255
+ }
7256
+ return { endpoint, key, afterId: stored, resumed: true };
7257
+ }
7258
+ function advanceFeedCursor(store, cursor, rawMeta) {
7259
+ if (cursor.afterId === undefined) {
7260
+ return undefined;
7261
+ }
7262
+ const meta = readObject(rawMeta);
7263
+ const nextAfterId = readNumber(meta?.next_after_id) ?? cursor.afterId;
7264
+ store.set(cursor.key, nextAfterId);
7265
+ return {
7266
+ endpoint: cursor.endpoint,
7267
+ mode: cursor.resumed ? "resumed" : "seeded",
7268
+ used_after_id: cursor.afterId,
7269
+ next_after_id: nextAfterId,
7270
+ has_more: readBoolean(meta?.has_more) ?? false,
7271
+ stored_in_memory: true
7272
+ };
7273
+ }
7274
+ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
7275
+ const startedAt = Date.now();
7276
+ const results = [];
7277
+ const sourceRequests = [];
7278
+ let currentCursor = initialCursor;
7279
+ let lastApi;
7280
+ let lastData;
7281
+ let cursorPayload;
7282
+ let pagesFetched = 0;
7283
+ let hasMore = false;
7284
+ let pageBudgetExhausted = false;
7285
+ let timeBudgetExhausted = false;
7286
+ do {
7287
+ const api = await fetchPage(currentCursor.afterId);
7288
+ const data = readObject(api.data);
7289
+ const pageResults = readArray(data?.results);
7290
+ lastApi = api;
7291
+ lastData = data;
7292
+ results.push(...pageResults);
7293
+ pagesFetched += 1;
7294
+ sourceRequests.push({
7295
+ source: currentCursor.endpoint,
7296
+ page: pagesFetched,
7297
+ request_id: api.requestId,
7298
+ upstream_request_id: api.upstreamRequestId,
7299
+ ...apiSourceMetadata(api)
7300
+ });
7301
+ cursorPayload = advanceFeedCursor(store, currentCursor, data?.meta);
7302
+ hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
7303
+ if (currentCursor.afterId === undefined || !hasMore) {
7304
+ break;
7305
+ }
7306
+ if (pagesFetched >= maxPages) {
7307
+ pageBudgetExhausted = true;
7308
+ break;
7309
+ }
7310
+ if (Date.now() - startedAt >= timeBudgetMs) {
7311
+ timeBudgetExhausted = true;
7312
+ break;
7313
+ }
7314
+ currentCursor = {
7315
+ ...currentCursor,
7316
+ afterId: readNumber(cursorPayload?.next_after_id) ?? currentCursor.afterId,
7317
+ resumed: true
7318
+ };
7319
+ } while (true);
7320
+ if (!lastApi) {
7321
+ throw new Error(`No ${initialCursor.endpoint} response was collected.`);
7322
+ }
7323
+ return {
7324
+ lastApi,
7325
+ lastData,
7326
+ results,
7327
+ cursor: cursorPayload,
7328
+ pagesFetched,
7329
+ pageBudgetExhausted,
7330
+ timeBudgetExhausted,
7331
+ sourceRequests
7332
+ };
7333
+ }
7334
+ function normalizeFeedCursorFilters(filters) {
7335
+ return Object.fromEntries(Object.entries(filters)
7336
+ .filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
7337
+ .map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
7338
+ }
7339
+ function normalizeFeedCursorFilterValue(value) {
7340
+ if (Array.isArray(value)) {
7341
+ return value
7342
+ .map((entry) => normalizeFeedCursorFilterValue(entry))
7343
+ .sort((left, right) => String(left).localeCompare(String(right)));
7344
+ }
7345
+ return typeof value === "string" ? value.trim().toLowerCase() : value;
7346
+ }
6424
7347
  async function runTool(toolName, config, rateLimiter, callback) {
6425
7348
  const startedAt = Date.now();
6426
7349
  const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
@@ -6434,7 +7357,7 @@ async function runTool(toolName, config, rateLimiter, callback) {
6434
7357
  },
6435
7358
  mcp_rate_limit: toRateLimitPayload(decision),
6436
7359
  mcp_timing: toolTimingPayload(toolName, durationMs)
6437
- }, true);
7360
+ }, true, config.compactToolText);
6438
7361
  }
6439
7362
  try {
6440
7363
  const payload = await callback();
@@ -6444,21 +7367,21 @@ async function runTool(toolName, config, rateLimiter, callback) {
6444
7367
  ...payload,
6445
7368
  mcp_rate_limit: toRateLimitPayload(decision),
6446
7369
  mcp_timing: toolTimingPayload(toolName, durationMs)
6447
- });
7370
+ }, false, config.compactToolText);
6448
7371
  }
6449
7372
  catch (error) {
6450
7373
  const durationMs = Date.now() - startedAt;
6451
7374
  recordToolMetric(toolName, durationMs, "error");
6452
- return toolResult(errorPayload(error, decision, toolName, durationMs), true);
7375
+ return toolResult(errorPayload(error, decision, toolName, durationMs), true, config.compactToolText);
6453
7376
  }
6454
7377
  }
6455
- function toolResult(payload, isError = false) {
7378
+ function toolResult(payload, isError = false, compactText = false) {
6456
7379
  const resourceLinks = collectResourceLinks(payload);
6457
7380
  return {
6458
7381
  content: [
6459
7382
  {
6460
7383
  type: "text",
6461
- text: JSON.stringify(payload)
7384
+ text: compactText ? compactToolText(payload, isError) : JSON.stringify(payload)
6462
7385
  },
6463
7386
  ...resourceLinks
6464
7387
  ],
@@ -6466,6 +7389,29 @@ function toolResult(payload, isError = false) {
6466
7389
  ...(isError ? { isError: true } : {})
6467
7390
  };
6468
7391
  }
7392
+ function compactToolText(payload, isError) {
7393
+ if (isError) {
7394
+ return JSON.stringify(stripUndefined({
7395
+ request_id: payload.request_id,
7396
+ error: payload.error,
7397
+ mcp_timing: payload.mcp_timing
7398
+ }));
7399
+ }
7400
+ const meta = readObject(payload.meta);
7401
+ const returned = numberField(meta, "returned") ??
7402
+ [
7403
+ readArray(payload.programs).length,
7404
+ readArray(payload.targets).length,
7405
+ readArray(payload.changes).length,
7406
+ readArray(payload.repository_changes).length
7407
+ ].find((count) => count > 0);
7408
+ return JSON.stringify(stripUndefined({
7409
+ request_id: payload.request_id,
7410
+ summary: "Full tool result is available in structuredContent.",
7411
+ returned,
7412
+ mcp_timing: payload.mcp_timing
7413
+ }));
7414
+ }
6469
7415
  function promptResult(text) {
6470
7416
  return {
6471
7417
  messages: [
@@ -6488,6 +7434,7 @@ function withApiMetadata(api, payload) {
6488
7434
  upstream_request_id: api.upstreamRequestId,
6489
7435
  cache: stripUndefined({
6490
7436
  hit: api.cached,
7437
+ stale_while_revalidate: api.stale,
6491
7438
  coalesced_live_request: api.coalesced,
6492
7439
  expires_at: api.cacheExpiresAt
6493
7440
  }),
@@ -6605,18 +7552,25 @@ function compactRecord(values) {
6605
7552
  }));
6606
7553
  }
6607
7554
  function latestAddedTargetsQuery(input) {
6608
- return {
7555
+ return stripUndefined({
6609
7556
  change_type: "added",
6610
7557
  target_type: input.target_type,
7558
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
7559
+ min_reward: input.min_reward,
7560
+ reward_threshold_mode: input.reward_threshold_mode,
7561
+ max_public_report_count: input.max_public_report_count,
7562
+ require_known_report_count: input.require_known_report_count,
7563
+ include_unknown_report_count: input.include_unknown_report_count,
6611
7564
  include_out_of_scope: input.include_out_of_scope,
6612
7565
  include_ineligible: input.include_ineligible,
6613
7566
  recent_changes_page_size: input.recent_changes_page_size,
7567
+ max_results: input.max_results,
6614
7568
  include_full_target_list: input.include_full_target_list,
6615
7569
  full_target_list_mode: input.full_target_list_mode,
6616
7570
  full_target_limit: input.full_target_limit,
6617
7571
  full_list_include_out_of_scope: input.full_list_include_out_of_scope,
6618
7572
  full_list_include_ineligible: input.full_list_include_ineligible
6619
- };
7573
+ });
6620
7574
  }
6621
7575
  function formatTargetList(targets, mode) {
6622
7576
  if (mode === "identifiers") {
@@ -6633,7 +7587,11 @@ function compactTarget(target) {
6633
7587
  target_type: stringField(target, "target_type"),
6634
7588
  in_scope: readBoolean(target.in_scope),
6635
7589
  eligible_for_bounty: readBoolean(target.eligible_for_bounty),
6636
- wildcard: readBoolean(target.wildcard)
7590
+ wildcard: readBoolean(target.wildcard),
7591
+ target_opportunity_score: readNumber(target.target_opportunity_score),
7592
+ target_opportunity_label: stringField(target, "target_opportunity_label"),
7593
+ competition_risk: stringField(target, "competition_risk"),
7594
+ why_this_target: stringField(target, "why_this_target")
6637
7595
  });
6638
7596
  }
6639
7597
  function targetIdentifier(target) {