@bbradar/mcp 0.1.20 → 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.19";
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;
@@ -29,10 +30,13 @@ 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
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;
37
41
  const FAST_RESOLVE_SEARCH_QUERIES = 2;
38
42
  const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
@@ -45,6 +49,7 @@ const CORE_TOOL_NAMES = new Set([
45
49
  "get_program_eligible_targets_by_name",
46
50
  "get_program_targets",
47
51
  "get_recent_changes",
52
+ "get_repository_changes",
48
53
  "get_latest_added_targets",
49
54
  "get_opportunities",
50
55
  "find_programs",
@@ -54,6 +59,11 @@ const CORE_TOOL_NAMES = new Set([
54
59
  "get_mcp_status",
55
60
  "export_targets"
56
61
  ]);
62
+ const TOOL_ENDPOINT_REQUIREMENTS = {
63
+ get_repository_changes: "repository_changes",
64
+ export_targets: "target_export",
65
+ get_opportunities: "opportunities"
66
+ };
57
67
  const PROGRAM_ID_PLATFORM_GUESSES = [
58
68
  "HackerOne",
59
69
  "BugCrowd",
@@ -159,6 +169,7 @@ const TARGET_QUERY_STOP_WORDS = new Set([
159
169
  ]);
160
170
  const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
161
171
  const changeListModeSchema = z.enum(["compact", "full"]);
172
+ const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
162
173
  const programListModeSchema = z.enum(["compact", "full"]);
163
174
  const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
164
175
  const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
@@ -227,6 +238,8 @@ const pageSchema = z.number().int().min(1).default(1);
227
238
  const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
228
239
  const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
229
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"]);
230
243
  const programSortSchema = z
231
244
  .enum([
232
245
  "best_match",
@@ -278,6 +291,7 @@ function toolTimingOutputSchema() {
278
291
  function cacheOutputSchema() {
279
292
  return z.object({
280
293
  hit: z.boolean(),
294
+ stale_while_revalidate: z.boolean().optional(),
281
295
  coalesced_live_request: z.boolean().optional(),
282
296
  expires_at: z.string().optional()
283
297
  });
@@ -364,10 +378,12 @@ const programCandidatePresetInputSchema = {
364
378
  output_mode: programListModeSchema.default("compact"),
365
379
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
366
380
  };
367
- function createProfiledToolRegistrar(server, profile) {
381
+ function createProfiledToolRegistrar(server, profile, client) {
368
382
  return (name, toolConfig, callback) => {
369
383
  const registeredTool = server.registerTool(name, toolConfig, callback);
370
- if (profile === "core" && !CORE_TOOL_NAMES.has(name)) {
384
+ const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
385
+ if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
386
+ (requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
371
387
  registeredTool.disable();
372
388
  }
373
389
  return registeredTool;
@@ -391,7 +407,9 @@ export function createBbradarServer(client, config) {
391
407
  "Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
392
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.",
393
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.",
394
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.",
412
+ "Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
395
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.",
396
414
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
397
415
  "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
@@ -402,13 +420,26 @@ export function createBbradarServer(client, config) {
402
420
  }, {
403
421
  instructions: instructions.join("\n")
404
422
  });
405
- const registerTool = createProfiledToolRegistrar(server, config.toolProfile);
423
+ const registerTool = createProfiledToolRegistrar(server, config.toolProfile, client);
406
424
  const rateLimiter = new FixedWindowRateLimiter();
407
425
  const exportStore = new Map();
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);
408
438
  registerTool("search_programs", {
409
439
  title: "Search BBRadar Programs",
410
- 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}.`,
411
441
  inputSchema: {
442
+ search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
412
443
  platforms: platformListSchema(),
413
444
  tags: tagListSchema(),
414
445
  updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
@@ -505,8 +536,8 @@ export function createBbradarServer(client, config) {
505
536
  language_tags: languageTagListSchema(),
506
537
  include_out_of_scope: z.boolean().default(false),
507
538
  include_ineligible: z.boolean().default(false),
508
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
509
- 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)),
510
541
  group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
511
542
  target_list_mode: targetListModeSchema.default("identifiers"),
512
543
  max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
@@ -549,6 +580,7 @@ export function createBbradarServer(client, config) {
549
580
  resolved_program: jsonRecordSchema().optional(),
550
581
  program_id: z.string().optional(),
551
582
  targets: z.array(z.unknown()).optional(),
583
+ continuation: jsonRecordSchema().optional(),
552
584
  warnings: z.array(z.string()).optional(),
553
585
  upstream_budget: jsonRecordSchema().optional(),
554
586
  ranking_scope: jsonRecordSchema().optional(),
@@ -558,14 +590,19 @@ export function createBbradarServer(client, config) {
558
590
  }, (args) => runTool("get_program_eligible_targets_by_name", config, rateLimiter, async () => getProgramEligibleTargetsByName(client, config, args)));
559
591
  registerTool("get_program_targets", {
560
592
  title: "Get Program Targets",
561
- 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}.`,
562
594
  inputSchema: {
563
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(),
564
601
  include_out_of_scope: z.boolean().default(false),
565
602
  include_ineligible: z.boolean().default(false),
566
603
  strict_scope_filter: z.boolean().default(true),
567
604
  offset: z.number().int().min(0).default(0),
568
- 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)),
569
606
  output_mode: targetListModeSchema.default("compact")
570
607
  },
571
608
  outputSchema: {
@@ -576,13 +613,21 @@ export function createBbradarServer(client, config) {
576
613
  },
577
614
  annotations: readOnlyAnnotations
578
615
  }, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
579
- 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);
580
618
  }));
581
619
  registerTool("get_recent_changes", {
582
620
  title: "Get Recent Target Changes",
583
621
  description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
584
622
  inputSchema: {
585
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(),
586
631
  include_removed: z.boolean().default(false),
587
632
  include_ineligible: z.boolean().default(false),
588
633
  include_out_of_scope: z.boolean().default(false),
@@ -597,25 +642,102 @@ export function createBbradarServer(client, config) {
597
642
  ...apiEnvelopeOutputShape(),
598
643
  changes: z.array(jsonRecordSchema()).optional(),
599
644
  meta: jsonRecordSchema().optional(),
600
- facets: jsonRecordSchema().optional()
645
+ facets: jsonRecordSchema().optional(),
646
+ cursor: jsonRecordSchema().optional(),
647
+ continuation: jsonRecordSchema().optional()
601
648
  },
602
649
  annotations: readOnlyAnnotations
603
650
  }, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
604
- const { output_mode, ...filters } = args;
605
- const api = await client.getRecentChanges(toQuery(filters));
606
- 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;
607
664
  const sanitizedMeta = readObject(sanitizeJson(data?.meta));
608
- const changes = readArray(data?.results)
665
+ const changes = collected.results
609
666
  .map((change) => sanitizeChange(change, config.webBaseUrl))
610
667
  .map((change) => formatChange(change, output_mode));
611
- return withApiMetadata(api, {
668
+ return applyCollectionContinuation(withApiMetadata(api, {
612
669
  changes,
613
670
  meta: stripUndefined({
614
671
  ...(sanitizedMeta ?? {}),
615
- 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
616
677
  }),
617
- facets: sanitizeJson(data?.facets)
618
- });
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);
619
741
  }));
620
742
  registerTool("get_latest_added_targets", {
621
743
  title: "Get Latest Added Targets",
@@ -630,7 +752,7 @@ export function createBbradarServer(client, config) {
630
752
  include_unknown_report_count: z.boolean().default(true),
631
753
  include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
632
754
  include_ineligible: z.boolean().default(false).describe("Include ineligible."),
633
- recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
755
+ recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
634
756
  max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
635
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."),
636
758
  full_target_list_mode: targetListModeSchema.default("identifiers"),
@@ -701,8 +823,8 @@ export function createBbradarServer(client, config) {
701
823
  include_removed: z.boolean().default(true),
702
824
  include_out_of_scope: z.boolean().default(false),
703
825
  include_ineligible: z.boolean().default(false),
704
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
705
- 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)),
706
828
  target_list_mode: targetListModeSchema.default("identifiers")
707
829
  },
708
830
  outputSchema: {
@@ -728,7 +850,7 @@ export function createBbradarServer(client, config) {
728
850
  platforms: platformListSchema(),
729
851
  tags: tagListSchema(),
730
852
  language_tags: languageTagListSchema(),
731
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
853
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
732
854
  max_programs: z.number().int().min(1).max(50).default(10),
733
855
  sample_size: z.number().int().min(1).max(20).default(5),
734
856
  target_list_mode: targetListModeSchema.default("identifiers")
@@ -749,8 +871,8 @@ export function createBbradarServer(client, config) {
749
871
  since: isoDateTimeSchema.optional().describe("ISO datetime."),
750
872
  target_type: filterValueSchema().optional(),
751
873
  include_ineligible: z.boolean().default(false).describe("Include ineligible."),
752
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
753
- 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)),
754
876
  target_list_mode: targetListModeSchema.default("identifiers")
755
877
  },
756
878
  outputSchema: {
@@ -774,7 +896,7 @@ export function createBbradarServer(client, config) {
774
896
  since: isoDateTimeSchema.optional(),
775
897
  target_type: filterValueSchema().optional(),
776
898
  include_ineligible: z.boolean().default(false),
777
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
899
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
778
900
  max_targets_per_program: z.number().int().min(1).max(50).default(10),
779
901
  target_list_mode: targetListModeSchema.default("identifiers")
780
902
  },
@@ -820,14 +942,16 @@ export function createBbradarServer(client, config) {
820
942
  }));
821
943
  registerTool("find_programs", {
822
944
  title: "Find BBRadar Programs",
823
- 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.",
824
946
  inputSchema: {
825
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."),
826
949
  tags: tagListSchema(),
827
950
  scope_tags: scopeTagListSchema(),
828
951
  target_types: targetTypeListSchema(),
829
952
  language_tags: languageTagListSchema(),
830
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."),
831
955
  opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
832
956
  updated_since: isoDateTimeSchema.optional(),
833
957
  min_bounty_min: z.number().int().min(0).optional(),
@@ -1211,7 +1335,7 @@ export function createBbradarServer(client, config) {
1211
1335
  platforms: platformListSchema(),
1212
1336
  tags: tagListSchema(),
1213
1337
  language_tags: languageTagListSchema(),
1214
- page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
1338
+ page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
1215
1339
  max_programs: z.number().int().min(1).max(50).default(10),
1216
1340
  sample_size: z.number().int().min(1).max(20).default(5),
1217
1341
  target_list_mode: targetListModeSchema.default("identifiers")
@@ -1387,7 +1511,7 @@ export function createBbradarServer(client, config) {
1387
1511
  annotations: readOnlyAnnotations
1388
1512
  }, (args) => runTool("get_bbradar_guide", config, rateLimiter, async () => ({
1389
1513
  request_id: randomUUID(),
1390
- guide: bbradarGuide(args.topic, config.toolProfile)
1514
+ guide: bbradarGuide(args.topic, config.toolProfile, client)
1391
1515
  })));
1392
1516
  registerTool("get_mcp_status", {
1393
1517
  title: "Get BBRadar MCP Status",
@@ -1417,6 +1541,13 @@ export function createBbradarServer(client, config) {
1417
1541
  compact_tool_text: config.compactToolText,
1418
1542
  tool_profile: config.toolProfile,
1419
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
+ },
1420
1551
  validate_on_startup: config.validateOnStartup,
1421
1552
  transport: "stdio",
1422
1553
  remote_streamable_http: "not_enabled_for_local_setup",
@@ -1626,7 +1757,7 @@ export function createBbradarServer(client, config) {
1626
1757
  include_removed: z.boolean().default(true),
1627
1758
  include_ineligible: z.boolean().default(false),
1628
1759
  include_out_of_scope: z.boolean().default(false),
1629
- page_size: recentChangesPageSizeSchema.default(50)
1760
+ page_size: recentChangesPageSizeSchema.default(Math.min(50, maxTargetChangesPageSize))
1630
1761
  },
1631
1762
  outputSchema: {
1632
1763
  ...apiEnvelopeOutputShape(),
@@ -1638,16 +1769,16 @@ export function createBbradarServer(client, config) {
1638
1769
  }, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
1639
1770
  registerTool("export_targets", {
1640
1771
  title: "Export BBRadar Targets",
1641
- description: `Read-only target export. limit max ${MAX_TARGET_EXPORT}.`,
1772
+ description: `Read-only target export. limit max ${maxTargetExport}.`,
1642
1773
  inputSchema: {
1643
- program_ids: z.array(programIdentifierSchema).max(50).default([]),
1774
+ program_ids: z.array(programIdentifierSchema).max(maxTargetExportProgramIds).default([]),
1644
1775
  platforms: platformListSchema(),
1645
1776
  tags: tagListSchema(),
1646
1777
  opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
1647
1778
  include_out_of_scope: z.boolean().default(false),
1648
1779
  include_ineligible: z.boolean().default(false),
1649
1780
  format: z.enum(["json", "csv"]).default("json"),
1650
- 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)
1651
1782
  },
1652
1783
  outputSchema: {
1653
1784
  ...apiEnvelopeOutputShape(),
@@ -1694,12 +1825,12 @@ export function createBbradarServer(client, config) {
1694
1825
  }
1695
1826
  }));
1696
1827
  registerPrompts(server, config.toolProfile);
1697
- registerResources(server, client, config, exportStore);
1828
+ registerResources(server, client, config, exportStore, continuationStore);
1698
1829
  return server;
1699
1830
  }
1700
- function bbradarGuide(topic, profile) {
1831
+ function bbradarGuide(topic, profile, client) {
1701
1832
  if (profile === "core") {
1702
- return coreBbradarGuide(topic);
1833
+ return coreBbradarGuide(topic, client);
1703
1834
  }
1704
1835
  const primaryEntryPoints = [
1705
1836
  {
@@ -1732,6 +1863,11 @@ function bbradarGuide(topic, profile) {
1732
1863
  first_tool: "find_program_candidates",
1733
1864
  then: "Use get_program_brief on the best program_id if the user wants details."
1734
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
+ },
1735
1871
  {
1736
1872
  user_goal: "named program summary",
1737
1873
  first_tool: "run_program_name_action",
@@ -1752,6 +1888,22 @@ function bbradarGuide(topic, profile) {
1752
1888
  first_tool: "find_recently_updated_programs",
1753
1889
  then: "Use get_latest_added_targets for global added targets, or check_program_new_targets when the user names one program."
1754
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
+ },
1755
1907
  {
1756
1908
  user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
1757
1909
  first_tool: "get_latest_added_targets",
@@ -1797,10 +1949,12 @@ function bbradarGuide(topic, profile) {
1797
1949
  };
1798
1950
  const filters = {
1799
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.",
1800
1953
  tags: "Broad combined tag filter across scope, language, and target-type signals.",
1801
1954
  scope_tags: "Scope category signals such as domain, api, mobile, smart-contract, wildcard.",
1802
1955
  target_types: "Target-kind signals such as domain, api, url, mobile, smart-contract.",
1803
1956
  language_tags: "Technology/language signals such as javascript, solidity, web3, php, python.",
1957
+ exclude_web3: "Set true for Web2-only requests.",
1804
1958
  vdp_mode: "Default exclude. Include VDP/no-bounty data only when asked."
1805
1959
  };
1806
1960
  const examples = [
@@ -1851,19 +2005,25 @@ function bbradarGuide(topic, profile) {
1851
2005
  require_bounty: true
1852
2006
  },
1853
2007
  primary_entry_points: topic === "start" ? primaryEntryPoints : primaryEntryPoints.filter((entry) => entry.tool !== "get_bbradar_guide"),
1854
- 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))),
1855
2010
  filters,
1856
2011
  advanced_tools: advancedTools,
1857
2012
  examples
1858
2013
  };
1859
2014
  }
1860
- function coreBbradarGuide(topic) {
2015
+ function coreBbradarGuide(topic, client) {
1861
2016
  const decisionTree = [
1862
2017
  {
1863
2018
  user_goal: "find programs to hunt",
1864
2019
  first_tool: "find_programs",
1865
2020
  arguments: { max_results: 10, output_mode: "compact" }
1866
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
+ },
1867
2027
  {
1868
2028
  user_goal: "named program summary or action",
1869
2029
  first_tool: "run_program_name_action",
@@ -1884,6 +2044,21 @@ function coreBbradarGuide(topic) {
1884
2044
  first_tool: "get_recent_changes",
1885
2045
  then: "Use get_latest_added_targets when only newly added targets are needed."
1886
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
+ },
1887
2062
  {
1888
2063
  user_goal: "export targets",
1889
2064
  first_tool: "export_targets",
@@ -1914,10 +2089,15 @@ function coreBbradarGuide(topic) {
1914
2089
  "find_programs_by_target",
1915
2090
  "get_bbradar_guide"
1916
2091
  ],
1917
- decision_tree: decisionTree.filter((entry) => topic === "start" || guideEntryMatchesTopic(topic, entry)),
1918
- available_tools: [...CORE_TOOL_NAMES]
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))
1919
2095
  };
1920
2096
  }
2097
+ function toolAvailableForContract(client, toolName) {
2098
+ const endpoint = TOOL_ENDPOINT_REQUIREMENTS[toolName];
2099
+ return endpoint === undefined || client.supportsEndpoint(endpoint);
2100
+ }
1921
2101
  function guideEntryMatchesTopic(topic, entry) {
1922
2102
  const goal = typeof entry.user_goal === "string" ? entry.user_goal : "";
1923
2103
  const firstTool = typeof entry.first_tool === "string" ? entry.first_tool : "";
@@ -1944,15 +2124,52 @@ function guideEntryMatchesTopic(topic, entry) {
1944
2124
  }
1945
2125
  return true;
1946
2126
  }
1947
- 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
+ });
1948
2164
  server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
1949
2165
  title: "BBRadar Target Export",
1950
- 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.",
1951
2167
  mimeType: "application/json"
1952
2168
  }, async (uri) => {
1953
2169
  const exportId = uri.searchParams.get("export_id") ?? "";
1954
- const payload = exportStore.get(exportId);
1955
- if (!payload) {
2170
+ pruneExpiredExports(exportStore);
2171
+ const storedExport = exportStore.get(exportId);
2172
+ if (!storedExport) {
1956
2173
  return jsonResource(uri.toString(), {
1957
2174
  request_id: randomUUID(),
1958
2175
  error: {
@@ -1963,7 +2180,7 @@ function registerResources(server, client, config, exportStore) {
1963
2180
  return jsonResource(uri.toString(), {
1964
2181
  request_id: randomUUID(),
1965
2182
  export_id: exportId,
1966
- export: payload
2183
+ export: storedExport.payload
1967
2184
  });
1968
2185
  });
1969
2186
  server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
@@ -3498,6 +3715,10 @@ function programsByTargetResult(input, warnings, sourceRequests, matches, budget
3498
3715
  });
3499
3716
  }
3500
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
+ }
3501
3722
  const searchQueries = targetApiSearchQueries(input.target_query);
3502
3723
  const grouped = new Map();
3503
3724
  let rawMatchesScanned = 0;
@@ -3519,7 +3740,7 @@ async function searchProgramsByTargetApi(client, config, input, budget, warnings
3519
3740
  include_out_of_scope: input.include_out_of_scope,
3520
3741
  include_ineligible: input.include_ineligible,
3521
3742
  page: 1,
3522
- 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))
3523
3744
  });
3524
3745
  refundBudgetIfNoUpstreamRequest(budget, api);
3525
3746
  }
@@ -4096,8 +4317,18 @@ function latestAddedProgramMatches(change, input, platformFilters) {
4096
4317
  return true;
4097
4318
  }
4098
4319
  async function getProgramTargetsPayload(client, input, programId) {
4099
- 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
+ });
4100
4330
  const data = readObject(api.data);
4331
+ const upstreamMeta = readObject(data?.meta);
4101
4332
  const rawTargets = readArray(data?.targets);
4102
4333
  const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
4103
4334
  const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
@@ -4108,10 +4339,13 @@ async function getProgramTargetsPayload(client, input, programId) {
4108
4339
  offset: input.offset,
4109
4340
  limit: input.limit,
4110
4341
  output_mode: input.output_mode,
4111
- total_active_targets: rawTargets.length,
4342
+ total_active_targets: readNumber(upstreamMeta?.total) ?? rawTargets.length,
4112
4343
  total_after_filters: sanitizedTargets.length,
4113
4344
  returned: targets.length,
4114
- 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),
4115
4349
  target_source: "program_targets"
4116
4350
  }
4117
4351
  });
@@ -5196,6 +5430,79 @@ function programTargetsResourceUri(programId) {
5196
5430
  function exportResourceUri(exportId) {
5197
5431
  return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
5198
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
+ }
5199
5506
  function jsonResource(uri, payload) {
5200
5507
  return {
5201
5508
  contents: [
@@ -5208,41 +5515,86 @@ function jsonResource(uri, payload) {
5208
5515
  };
5209
5516
  }
5210
5517
  function rememberExport(exportStore, exportId, payload) {
5518
+ const now = Date.now();
5519
+ const byteLength = Buffer.byteLength(JSON.stringify(payload), "utf8");
5520
+ pruneExpiredExports(exportStore, now);
5211
5521
  if (exportStore.has(exportId)) {
5212
5522
  exportStore.delete(exportId);
5213
5523
  }
5214
- while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES) {
5524
+ while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES ||
5525
+ retainedExportBytes(exportStore) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
5215
5526
  const oldestKey = exportStore.keys().next().value;
5216
5527
  if (oldestKey === undefined) {
5217
5528
  break;
5218
5529
  }
5219
5530
  exportStore.delete(oldestKey);
5220
5531
  }
5221
- 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;
5222
5551
  }
5223
5552
  function previewExportPayload(payload) {
5553
+ if (typeof payload === "string") {
5554
+ return previewExportText(payload);
5555
+ }
5224
5556
  if (Array.isArray(payload)) {
5225
5557
  return payload.slice(0, EXPORT_PREVIEW_LIMIT);
5226
5558
  }
5227
- if (readObject(payload) && Array.isArray(payload.targets)) {
5559
+ const object = readObject(payload);
5560
+ if (object && typeof object.content === "string") {
5228
5561
  return {
5229
- ...payload,
5230
- targets: payload.targets.slice(0, EXPORT_PREVIEW_LIMIT)
5562
+ ...object,
5563
+ content: previewExportText(object.content)
5564
+ };
5565
+ }
5566
+ if (object && Array.isArray(object.targets)) {
5567
+ return {
5568
+ ...object,
5569
+ targets: object.targets.slice(0, EXPORT_PREVIEW_LIMIT)
5231
5570
  };
5232
5571
  }
5233
5572
  return payload;
5234
5573
  }
5235
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
+ }
5236
5578
  if (Array.isArray(payload)) {
5237
5579
  return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
5238
5580
  }
5239
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
+ }
5240
5585
  const targets = object?.targets;
5241
5586
  if (Array.isArray(targets)) {
5242
5587
  return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
5243
5588
  }
5244
5589
  return payload === undefined || payload === null ? 0 : 1;
5245
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
+ }
5246
5598
  function collectResourceLinks(payload) {
5247
5599
  const links = new Map();
5248
5600
  for (const program of extractProgramsFromPayload(payload)) {
@@ -5284,6 +5636,19 @@ function collectResourceLinks(payload) {
5284
5636
  mimeType: "application/json"
5285
5637
  });
5286
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
+ }
5287
5652
  return [...links.values()];
5288
5653
  }
5289
5654
  function extractProgramsFromPayload(payload) {
@@ -5333,9 +5698,18 @@ function compactProgram(program) {
5333
5698
  handle: stringField(program, "handle"),
5334
5699
  name: stringField(program, "name"),
5335
5700
  platform: stringField(program, "platform"),
5701
+ catalog: stringField(program, "catalog"),
5702
+ program_type: stringField(program, "program_type"),
5336
5703
  platform_url: stringField(program, "platform_url"),
5337
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),
5338
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),
5339
5713
  reward_range: readObject(program.reward_range),
5340
5714
  public_report_count: readNumber(program.public_report_count),
5341
5715
  public_report_count_label: stringField(program, "public_report_count_label"),
@@ -5385,6 +5759,30 @@ function formatChange(change, mode) {
5385
5759
  target: target ? compactTarget(target) : undefined
5386
5760
  });
5387
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
+ }
5388
5786
  function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
5389
5787
  const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
5390
5788
  const groups = {
@@ -5636,6 +6034,14 @@ function candidateLooksContestLike(candidate) {
5636
6034
  return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
5637
6035
  }
5638
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
+ }
5639
6045
  if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
5640
6046
  return false;
5641
6047
  }
@@ -5824,6 +6230,16 @@ function normalizeFindProgramsInput(input) {
5824
6230
  const warnings = [];
5825
6231
  const normalized = { ...input };
5826
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
+ }
5827
6243
  normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
5828
6244
  normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
5829
6245
  normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
@@ -5890,8 +6306,10 @@ async function findPrograms(client, config, input) {
5890
6306
  warnings: warnings.length > 0 ? warnings : undefined,
5891
6307
  filters: stripUndefined({
5892
6308
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
6309
+ exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
5893
6310
  tags: upstreamTags.length > 0 ? upstreamTags : undefined,
5894
6311
  web3: input.web3 ? true : undefined,
6312
+ exclude_web3: input.exclude_web3 ? true : undefined,
5895
6313
  opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
5896
6314
  updated_since: input.updated_since,
5897
6315
  min_bounty_min: input.min_bounty_min,
@@ -6423,12 +6841,19 @@ function programCandidateMatches(candidate, filters) {
6423
6841
  if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
6424
6842
  return false;
6425
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
+ }
6426
6848
  if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
6427
6849
  return false;
6428
6850
  }
6429
6851
  if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
6430
6852
  return false;
6431
6853
  }
6854
+ if (filters.exclude_web3 && candidateHasWeb3Surface(candidate)) {
6855
+ return false;
6856
+ }
6432
6857
  if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
6433
6858
  return false;
6434
6859
  }
@@ -6441,7 +6866,9 @@ function programCandidateMatches(candidate, filters) {
6441
6866
  if (candidate.wildcardCount < filters.min_wildcards) {
6442
6867
  return false;
6443
6868
  }
6444
- 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) {
6445
6872
  return false;
6446
6873
  }
6447
6874
  if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
@@ -6812,6 +7239,111 @@ The plan must:
6812
7239
  - Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
6813
7240
  - Include a short checklist of BBRadar follow-up tool calls and decision points.`));
6814
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
+ }
6815
7347
  async function runTool(toolName, config, rateLimiter, callback) {
6816
7348
  const startedAt = Date.now();
6817
7349
  const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
@@ -6870,7 +7402,8 @@ function compactToolText(payload, isError) {
6870
7402
  [
6871
7403
  readArray(payload.programs).length,
6872
7404
  readArray(payload.targets).length,
6873
- readArray(payload.changes).length
7405
+ readArray(payload.changes).length,
7406
+ readArray(payload.repository_changes).length
6874
7407
  ].find((count) => count > 0);
6875
7408
  return JSON.stringify(stripUndefined({
6876
7409
  request_id: payload.request_id,
@@ -6901,6 +7434,7 @@ function withApiMetadata(api, payload) {
6901
7434
  upstream_request_id: api.upstreamRequestId,
6902
7435
  cache: stripUndefined({
6903
7436
  hit: api.cached,
7437
+ stale_while_revalidate: api.stale,
6904
7438
  coalesced_live_request: api.coalesced,
6905
7439
  expires_at: api.cacheExpiresAt
6906
7440
  }),
@@ -7053,7 +7587,11 @@ function compactTarget(target) {
7053
7587
  target_type: stringField(target, "target_type"),
7054
7588
  in_scope: readBoolean(target.in_scope),
7055
7589
  eligible_for_bounty: readBoolean(target.eligible_for_bounty),
7056
- 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")
7057
7595
  });
7058
7596
  }
7059
7597
  function targetIdentifier(target) {