@bbradar/mcp 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +47 -9
- package/benchmark/compare-revisions.mjs +164 -0
- package/benchmark/pro-benchmark.mjs +84 -0
- package/dist/bbradarClient.d.ts +16 -1
- package/dist/bbradarClient.js +213 -40
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +47 -13
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +237 -46
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
package/dist/server.js
CHANGED
|
@@ -5,8 +5,8 @@ import { BBRadarApiError } from "./bbradarClient.js";
|
|
|
5
5
|
import { NumericCursorStore } from "./cursorStore.js";
|
|
6
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
7
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
8
|
-
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
-
const SERVER_VERSION = "0.2.
|
|
8
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
+
const SERVER_VERSION = "0.2.6";
|
|
10
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
11
11
|
const MAX_TARGET_EXPORT = 500;
|
|
12
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -64,7 +64,14 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
64
64
|
"list_filters",
|
|
65
65
|
"get_bbradar_guide",
|
|
66
66
|
"get_mcp_status",
|
|
67
|
-
"export_targets"
|
|
67
|
+
"export_targets",
|
|
68
|
+
"get_history_changes",
|
|
69
|
+
"get_history_change_detail",
|
|
70
|
+
"get_program_history",
|
|
71
|
+
"get_program_activity",
|
|
72
|
+
"get_analytics_platform_series",
|
|
73
|
+
"get_analytics_program_activity",
|
|
74
|
+
"get_analytics_program_detail"
|
|
68
75
|
]);
|
|
69
76
|
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
70
77
|
search_targets: "target_search",
|
|
@@ -97,7 +104,6 @@ const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
|
97
104
|
];
|
|
98
105
|
const PRIMARY_PROGRAM_ID_PLATFORM_GUESSES = PROGRAM_ID_PLATFORM_GUESSES.slice(0, 8);
|
|
99
106
|
const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
|
|
100
|
-
const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
|
|
101
107
|
const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
|
|
102
108
|
const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
|
|
103
109
|
"program",
|
|
@@ -254,6 +260,9 @@ const stableSnapshotSchema = z
|
|
|
254
260
|
message: "Snapshot tokens cannot contain control characters."
|
|
255
261
|
})
|
|
256
262
|
.describe("Optional stable-page token. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Snapshot creation is consistency-first and can be slower; omit it for the fastest live result.");
|
|
263
|
+
const opaqueCursorSchema = z.string().min(1).max(4096).describe("Opaque five-minute cursor. Reuse with identical filters and page size; restart the query on expiry.");
|
|
264
|
+
const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
|
|
265
|
+
const proLimitSchema = z.number().int().min(1).max(100);
|
|
257
266
|
const pageSchema = z.number().int().min(1).default(1);
|
|
258
267
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
259
268
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
@@ -278,7 +287,7 @@ const programIdSchema = z
|
|
|
278
287
|
.string()
|
|
279
288
|
.trim()
|
|
280
289
|
.min(3)
|
|
281
|
-
.max(
|
|
290
|
+
.max(620)
|
|
282
291
|
.regex(/^[^:/?#\u0000-\u001F\u007F]+:[^?#\u0000-\u001F\u007F]+$/, "Use platform:handle, for example HackerOne:example-handle.")
|
|
283
292
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
284
293
|
message: "program_id cannot contain control characters."
|
|
@@ -313,6 +322,7 @@ function cacheOutputSchema() {
|
|
|
313
322
|
hit: z.boolean(),
|
|
314
323
|
stale_while_revalidate: z.boolean().optional(),
|
|
315
324
|
coalesced_live_request: z.boolean().optional(),
|
|
325
|
+
no_store: z.boolean().optional(),
|
|
316
326
|
expires_at: z.string().optional()
|
|
317
327
|
});
|
|
318
328
|
}
|
|
@@ -338,6 +348,7 @@ function apiEnvelopeOutputShape() {
|
|
|
338
348
|
mcp_timing: toolTimingOutputSchema().optional(),
|
|
339
349
|
warnings: z.array(z.string()).optional(),
|
|
340
350
|
program_id_resolution: jsonRecordSchema().optional(),
|
|
351
|
+
recovery: jsonRecordSchema().optional(),
|
|
341
352
|
error: errorOutputSchema().optional()
|
|
342
353
|
};
|
|
343
354
|
}
|
|
@@ -400,7 +411,11 @@ const programCandidatePresetInputSchema = {
|
|
|
400
411
|
};
|
|
401
412
|
function createProfiledToolRegistrar(server, profile, client) {
|
|
402
413
|
return (name, toolConfig, callback) => {
|
|
403
|
-
const
|
|
414
|
+
const scopedCallback = ((...args) => {
|
|
415
|
+
const extra = args[args.length - 1];
|
|
416
|
+
return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
|
|
417
|
+
});
|
|
418
|
+
const registeredTool = server.registerTool(name, toolConfig, scopedCallback);
|
|
404
419
|
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
405
420
|
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
406
421
|
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
@@ -417,6 +432,7 @@ export function createBbradarServer(client, config) {
|
|
|
417
432
|
"Use resolve_program or run_program_name_action for a named program.",
|
|
418
433
|
"Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
|
|
419
434
|
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
435
|
+
"For semantic changes use get_history_changes or get_program_history, then get_history_change_detail only when before/after items are needed. For exact activity use get_program_activity; for ranked activity use get_analytics_program_activity. Use daily analytics tools directly for date series.",
|
|
420
436
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
421
437
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
422
438
|
]
|
|
@@ -432,6 +448,7 @@ export function createBbradarServer(client, config) {
|
|
|
432
448
|
"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.",
|
|
433
449
|
"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.",
|
|
434
450
|
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
451
|
+
"Semantic History: get_history_changes globally or get_program_history for one exact ID, especially Self-Hosted. Fetch detail lazily with get_history_change_detail. Exact recorded signals: get_program_activity. Server-side rankings: get_analytics_program_activity. Daily series: get_analytics_platform_series or get_analytics_program_detail. Reuse opaque cursors only with unchanged filters and page size.",
|
|
435
452
|
"Discovery: find_hunt_targets is the one-call default; find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, and find_low_noise_programs are lower-level alternatives.",
|
|
436
453
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
437
454
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -459,11 +476,88 @@ export function createBbradarServer(client, config) {
|
|
|
459
476
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
460
477
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
461
478
|
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
479
|
+
const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
|
|
480
|
+
const analyticsOutput = { ...apiEnvelopeOutputShape(), platform: z.string().optional(), program: jsonRecordSchema().optional(), programs: z.array(jsonRecordSchema()).optional(), series: z.array(jsonRecordSchema()).optional(), observations: jsonRecordSchema().optional(), category: z.string().optional(), population: z.string().optional(), ranking_definition: z.string().optional(), inventory_definition: z.string().optional(), meta: jsonRecordSchema().optional() };
|
|
481
|
+
registerTool("get_history_changes", {
|
|
482
|
+
title: "Get Program History Changes",
|
|
483
|
+
description: "One page of semantic program History. Use get_program_history for an exact program, especially Self-Hosted. History may be partial or unavailable; inspect coverage.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90),
|
|
486
|
+
platform: z.string().min(1).max(100).optional(), program_id: programIdSchema.optional(),
|
|
487
|
+
category: z.enum(["scope", "rewards", "rules", "program", "repositories"]).optional(),
|
|
488
|
+
search: z.string().min(1).max(120).optional(), limit: proLimitSchema.default(50)
|
|
489
|
+
}, outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
490
|
+
}, (args) => runTool("get_history_changes", config, rateLimiter, async () => {
|
|
491
|
+
assertProFeature(client, "history_changes", "history_capture_enabled");
|
|
492
|
+
if (args.days === 0 && !args.program_id)
|
|
493
|
+
throw new Error("days=0 requires program_id; use get_program_history for Self-Hosted catalog IDs.");
|
|
494
|
+
const api = await client.getHistoryChanges(toQuery(args));
|
|
495
|
+
return proPage(api, "changes");
|
|
496
|
+
}));
|
|
497
|
+
registerTool("get_history_change_detail", {
|
|
498
|
+
title: "Get History Change Detail",
|
|
499
|
+
description: "Fetch one semantic History change's paginated before/after items only when needed.",
|
|
500
|
+
inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
501
|
+
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
502
|
+
}, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
|
|
503
|
+
assertProFeature(client, "history_change_detail");
|
|
504
|
+
assertHistoryCapture(client);
|
|
505
|
+
return proPage(await client.getHistoryChangeDetail(args.change_id, toQuery({ cursor: args.cursor, limit: args.limit })), "items");
|
|
506
|
+
}));
|
|
507
|
+
registerTool("get_program_history", {
|
|
508
|
+
title: "Get Program History",
|
|
509
|
+
description: "One page of exact semantic History for a platform:handle or Self-Hosted:slug catalog ID. Preserve archive id and catalog_id separately.",
|
|
510
|
+
inputSchema: { program_id: programIdSchema, cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90), limit: proLimitSchema.default(50) },
|
|
511
|
+
outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
512
|
+
}, (args) => runTool("get_program_history", config, rateLimiter, async () => {
|
|
513
|
+
assertProFeature(client, "program_history");
|
|
514
|
+
assertHistoryCapture(client);
|
|
515
|
+
return proPage(await client.getProgramHistory(args.program_id, toQuery({ cursor: args.cursor, days: args.days, limit: args.limit })), "changes");
|
|
516
|
+
}));
|
|
517
|
+
registerTool("get_program_activity", {
|
|
518
|
+
title: "Get Exact Program Activity",
|
|
519
|
+
description: "Read the authoritative activity_summary for one exact program ID.",
|
|
520
|
+
inputSchema: { program_id: programIdSchema },
|
|
521
|
+
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
522
|
+
}, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
|
|
523
|
+
assertProFeature(client, "program_activity");
|
|
524
|
+
return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
|
|
525
|
+
}));
|
|
526
|
+
registerTool("get_analytics_platform_series", {
|
|
527
|
+
title: "Get Platform Daily Analytics",
|
|
528
|
+
description: "One page of inclusive UTC daily platform analytics; inspect generation, projection_pending, and tracking_since.",
|
|
529
|
+
inputSchema: { platform: z.string().min(1).max(100), start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
530
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
531
|
+
}, (args) => runTool("get_analytics_platform_series", config, rateLimiter, async () => {
|
|
532
|
+
assertProFeature(client, "analytics_platform_series", "enabled");
|
|
533
|
+
const { platform, ...query } = args;
|
|
534
|
+
return proPage(await client.getAnalyticsPlatformSeries(platform, toQuery(query)), "series");
|
|
535
|
+
}));
|
|
536
|
+
registerTool("get_analytics_program_activity", {
|
|
537
|
+
title: "Rank Programs by Recorded Activity",
|
|
538
|
+
description: "Server-side daily activity ranking. One page; semantic activity excludes baselines and corrections.",
|
|
539
|
+
inputSchema: { start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), platform: z.string().min(1).max(100).optional(), category: z.enum(["all", "scope", "rewards", "rules", "schedule", "costs", "lifecycle"]).default("all"), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
540
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
541
|
+
}, (args) => runTool("get_analytics_program_activity", config, rateLimiter, async () => {
|
|
542
|
+
assertProFeature(client, "analytics_program_activity", "enabled");
|
|
543
|
+
return proPage(await client.getAnalyticsProgramActivity(toQuery(args)), "programs");
|
|
544
|
+
}));
|
|
545
|
+
registerTool("get_analytics_program_detail", {
|
|
546
|
+
title: "Get Program Daily Analytics",
|
|
547
|
+
description: "One page of program daily metrics, carried inventory state, report observations, and source coverage.",
|
|
548
|
+
inputSchema: { program_id: programIdSchema, start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
549
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
550
|
+
}, (args) => runTool("get_analytics_program_detail", config, rateLimiter, async () => {
|
|
551
|
+
assertProFeature(client, "analytics_program_detail", "enabled");
|
|
552
|
+
const { program_id, ...query } = args;
|
|
553
|
+
return proPage(await client.getAnalyticsProgramDetail(program_id, toQuery(query)), "series");
|
|
554
|
+
}));
|
|
462
555
|
registerTool("search_programs", {
|
|
463
556
|
title: "Search BBRadar Programs",
|
|
464
557
|
description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in because creating one can be slower than live indexed pagination.`,
|
|
465
558
|
inputSchema: {
|
|
466
559
|
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
560
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional(),
|
|
467
561
|
platforms: platformListSchema(),
|
|
468
562
|
tags: tagListSchema(),
|
|
469
563
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
@@ -718,16 +812,16 @@ export function createBbradarServer(client, config) {
|
|
|
718
812
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
719
813
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
720
814
|
assertStableSnapshotSupported(client, "target_changes", filters.snapshot);
|
|
721
|
-
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
815
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor, config);
|
|
722
816
|
const { min_target_score, target_opportunity_label, competition_risk, snapshot, ...upstreamFilters } = filters;
|
|
723
|
-
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRecentChanges(toQuery({
|
|
817
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRecentChanges(toQuery({
|
|
724
818
|
...upstreamFilters,
|
|
725
819
|
minTargetScore: min_target_score,
|
|
726
820
|
targetOpportunityLabel: target_opportunity_label,
|
|
727
821
|
competitionRisk: competition_risk,
|
|
728
822
|
snapshot: activeSnapshot,
|
|
729
823
|
after_id: afterId,
|
|
730
|
-
page: afterId === undefined ? filters.page : 1
|
|
824
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
731
825
|
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.searchTargets(toQuery({
|
|
732
826
|
q: upstreamFilters.search,
|
|
733
827
|
platforms: upstreamFilters.platforms,
|
|
@@ -795,13 +889,13 @@ export function createBbradarServer(client, config) {
|
|
|
795
889
|
}
|
|
796
890
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
797
891
|
assertStableSnapshotSupported(client, "repository_changes", filters.snapshot);
|
|
798
|
-
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
892
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor, config);
|
|
799
893
|
const { snapshot, ...upstreamFilters } = filters;
|
|
800
|
-
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRepositoryChanges(toQuery({
|
|
894
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRepositoryChanges(toQuery({
|
|
801
895
|
...upstreamFilters,
|
|
802
896
|
snapshot: activeSnapshot,
|
|
803
897
|
after_id: afterId,
|
|
804
|
-
page: afterId === undefined ? filters.page : 1
|
|
898
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
805
899
|
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.getRepositoryChanges(toQuery({
|
|
806
900
|
...upstreamFilters,
|
|
807
901
|
after_id: undefined,
|
|
@@ -1130,6 +1224,7 @@ export function createBbradarServer(client, config) {
|
|
|
1130
1224
|
target_types: targetTypeListSchema(),
|
|
1131
1225
|
language_tags: languageTagListSchema(),
|
|
1132
1226
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
1227
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional().describe("Source-classified program kind; sent to the Pro program list."),
|
|
1133
1228
|
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."),
|
|
1134
1229
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
1135
1230
|
updated_since: isoDateTimeSchema.optional(),
|
|
@@ -1144,7 +1239,7 @@ export function createBbradarServer(client, config) {
|
|
|
1144
1239
|
exclude_mobile_only: z.boolean().default(false),
|
|
1145
1240
|
exclude_non_web: z.boolean().default(false),
|
|
1146
1241
|
has_api_scope: z.boolean().default(false),
|
|
1147
|
-
contest_like: z.boolean().default(false).describe("Require
|
|
1242
|
+
contest_like: z.boolean().default(false).describe("Require source-classified contests."),
|
|
1148
1243
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
1149
1244
|
fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
|
|
1150
1245
|
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
@@ -1772,7 +1867,8 @@ export function createBbradarServer(client, config) {
|
|
|
1772
1867
|
...apiEnvelopeOutputShape(),
|
|
1773
1868
|
program: programOutputSchema().optional(),
|
|
1774
1869
|
activity: jsonRecordSchema().optional(),
|
|
1775
|
-
changes: z.array(jsonRecordSchema()).optional()
|
|
1870
|
+
changes: z.array(jsonRecordSchema()).optional(),
|
|
1871
|
+
meta: jsonRecordSchema().optional()
|
|
1776
1872
|
},
|
|
1777
1873
|
annotations: readOnlyAnnotations
|
|
1778
1874
|
}, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
|
|
@@ -1976,13 +2072,15 @@ export function createBbradarServer(client, config) {
|
|
|
1976
2072
|
const exportId = randomUUID();
|
|
1977
2073
|
const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
|
|
1978
2074
|
const resourceUri = exportResourceUri(exportId);
|
|
1979
|
-
|
|
2075
|
+
if (!api.noStore)
|
|
2076
|
+
rememberExport(exportStore, exportId, sanitizedExport);
|
|
1980
2077
|
return withApiMetadata(api, {
|
|
1981
2078
|
source_requests: resolvedProgramIds.sourceRequests.length > 0 ? resolvedProgramIds.sourceRequests : undefined,
|
|
1982
2079
|
warnings: resolvedProgramIds.warnings.length > 0 ? resolvedProgramIds.warnings : undefined,
|
|
1983
2080
|
export: {
|
|
1984
|
-
export_id: exportId,
|
|
1985
|
-
resource_uri: resourceUri,
|
|
2081
|
+
export_id: api.noStore ? undefined : exportId,
|
|
2082
|
+
resource_uri: api.noStore ? undefined : resourceUri,
|
|
2083
|
+
data: api.noStore ? sanitizedExport : undefined,
|
|
1986
2084
|
preview: previewExportPayload(sanitizedExport),
|
|
1987
2085
|
returned_preview_count: previewExportCount(sanitizedExport),
|
|
1988
2086
|
limit: args.limit
|
|
@@ -2079,6 +2177,9 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2079
2177
|
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
2080
2178
|
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
2081
2179
|
},
|
|
2180
|
+
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch get_history_change_detail only for a selected change." },
|
|
2181
|
+
{ user_goal: "most active programs in a date range", first_tool: "get_analytics_program_activity", arguments: {}, then: "Use this server-side ranking rather than sampling the program catalog." },
|
|
2182
|
+
{ user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2082
2183
|
{
|
|
2083
2184
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2084
2185
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2221,6 +2322,9 @@ function coreBbradarGuide(topic, client) {
|
|
|
2221
2322
|
first_tool: "get_repository_changes",
|
|
2222
2323
|
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2223
2324
|
},
|
|
2325
|
+
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
|
|
2326
|
+
{ user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
|
|
2327
|
+
{ user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2224
2328
|
{
|
|
2225
2329
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2226
2330
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2598,7 +2702,7 @@ function getCachedProgramFallback(config, requestedProgramId) {
|
|
|
2598
2702
|
return cloneProgramFallbackForRequest(cached.fallback, requestedProgramId, true);
|
|
2599
2703
|
}
|
|
2600
2704
|
function rememberProgramFallback(config, fallback) {
|
|
2601
|
-
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
|
|
2705
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(fallback.sourceRequests)) {
|
|
2602
2706
|
return;
|
|
2603
2707
|
}
|
|
2604
2708
|
pruneProgramFallbackCache();
|
|
@@ -2623,7 +2727,13 @@ function pruneProgramFallbackCache() {
|
|
|
2623
2727
|
}
|
|
2624
2728
|
}
|
|
2625
2729
|
function programFallbackCacheKey(config, requestedProgramId) {
|
|
2626
|
-
return `${config.apiBaseUrl}\n${normalizeTag(requestedProgramId)}`;
|
|
2730
|
+
return `${config.apiBaseUrl}\n${credentialCacheScope(config)}\n${isStrictProgramId(requestedProgramId) ? requestedProgramId : normalizeTag(requestedProgramId)}`;
|
|
2731
|
+
}
|
|
2732
|
+
function credentialCacheScope(config) {
|
|
2733
|
+
return createHash("sha256").update(config.apiKey).digest("hex");
|
|
2734
|
+
}
|
|
2735
|
+
function hasNoStoreSource(sourceRequests) {
|
|
2736
|
+
return sourceRequests.some((request) => readBoolean(request.no_store) === true);
|
|
2627
2737
|
}
|
|
2628
2738
|
function cloneProgramFallbackForRequest(fallback, requestedProgramId, fromCache) {
|
|
2629
2739
|
const warnings = fromCache
|
|
@@ -3077,7 +3187,8 @@ function getCachedResolveProgram(config, input) {
|
|
|
3077
3187
|
});
|
|
3078
3188
|
}
|
|
3079
3189
|
function rememberResolveProgram(config, input, payload) {
|
|
3080
|
-
|
|
3190
|
+
const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
|
|
3191
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(sourceRequests)) {
|
|
3081
3192
|
return;
|
|
3082
3193
|
}
|
|
3083
3194
|
pruneResolveProgramCache();
|
|
@@ -3103,7 +3214,8 @@ function pruneResolveProgramCache() {
|
|
|
3103
3214
|
function resolveProgramCacheKey(config, input) {
|
|
3104
3215
|
return stableStringify({
|
|
3105
3216
|
apiBaseUrl: config.apiBaseUrl,
|
|
3106
|
-
|
|
3217
|
+
credentialScope: credentialCacheScope(config),
|
|
3218
|
+
query: isStrictProgramId(input.query) ? input.query : normalizeTag(input.query),
|
|
3107
3219
|
platforms: input.platforms.map(normalizeTag).sort(),
|
|
3108
3220
|
tags: input.tags.map(normalizeTag).sort(),
|
|
3109
3221
|
opportunity_levels: [...input.opportunity_levels].sort(),
|
|
@@ -4311,8 +4423,9 @@ async function comparePrograms(client, config, input) {
|
|
|
4311
4423
|
});
|
|
4312
4424
|
}
|
|
4313
4425
|
async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
|
|
4314
|
-
const [programApi, changes] = await Promise.all([
|
|
4426
|
+
const [programApi, activityApi, changes] = await Promise.all([
|
|
4315
4427
|
client.getProgram(programId),
|
|
4428
|
+
client.supportsEndpoint("program_activity") ? client.getProgramActivity(programId) : Promise.resolve(undefined),
|
|
4316
4429
|
recentChangesLimit > 0
|
|
4317
4430
|
? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
|
|
4318
4431
|
: Promise.resolve(undefined)
|
|
@@ -4329,15 +4442,18 @@ async function summarizeProgramActivity(client, config, programId, recentChanges
|
|
|
4329
4442
|
upstream_request_id: programApi.upstreamRequestId,
|
|
4330
4443
|
...apiSourceMetadata(programApi)
|
|
4331
4444
|
},
|
|
4445
|
+
...(activityApi ? [{ source: "program_activity", request_id: activityApi.requestId, upstream_request_id: activityApi.upstreamRequestId, ...apiSourceMetadata(activityApi) }] : []),
|
|
4332
4446
|
...(changes?.sourceRequests ?? [])
|
|
4333
4447
|
],
|
|
4334
4448
|
program,
|
|
4335
4449
|
activity: stripUndefined({
|
|
4450
|
+
activity_summary: activityApi ? sanitizeProPayload(readObject(activityApi.data)?.activity_summary) : undefined,
|
|
4336
4451
|
target_counts: readObject(scopeSummary?.target_counts),
|
|
4337
4452
|
target_activity: targetActivity,
|
|
4338
4453
|
recent_change_count: changes?.changes.length
|
|
4339
4454
|
}),
|
|
4340
|
-
changes: changes?.changes ?? []
|
|
4455
|
+
changes: changes?.changes ?? [],
|
|
4456
|
+
meta: { target_change_samples_are_semantic_history: false, activity_source: activityApi ? "pro_activity_endpoint" : "legacy_program_projection" }
|
|
4341
4457
|
};
|
|
4342
4458
|
}
|
|
4343
4459
|
async function getLatestAddedTargets(client, config, input) {
|
|
@@ -4593,10 +4709,12 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
|
4593
4709
|
requestId: api.requestId,
|
|
4594
4710
|
upstreamRequestId: api.upstreamRequestId,
|
|
4595
4711
|
fetchedAt: api.fetchedAt,
|
|
4712
|
+
noStore: api.noStore,
|
|
4596
4713
|
cache: readObject(stripUndefined({
|
|
4597
4714
|
hit: api.cached,
|
|
4598
4715
|
coalesced_live_request: api.coalesced,
|
|
4599
|
-
expires_at: api.cacheExpiresAt
|
|
4716
|
+
expires_at: api.cacheExpiresAt,
|
|
4717
|
+
no_store: api.noStore
|
|
4600
4718
|
})),
|
|
4601
4719
|
source: "program_targets",
|
|
4602
4720
|
rawTargets,
|
|
@@ -4654,7 +4772,7 @@ function getCachedProgramTargetsFallback(client, programId) {
|
|
|
4654
4772
|
}
|
|
4655
4773
|
function rememberProgramTargetsFallback(client, programId, lookup) {
|
|
4656
4774
|
const diagnostics = client.diagnostics();
|
|
4657
|
-
if (!diagnostics.response_cache.enabled || lookup.unavailable) {
|
|
4775
|
+
if (!diagnostics.response_cache.enabled || lookup.unavailable || lookup.noStore) {
|
|
4658
4776
|
return;
|
|
4659
4777
|
}
|
|
4660
4778
|
const cache = programTargetsFallbackCacheForClient(client);
|
|
@@ -4692,7 +4810,7 @@ function pruneProgramTargetsFallbackCache(cache) {
|
|
|
4692
4810
|
}
|
|
4693
4811
|
}
|
|
4694
4812
|
function programTargetsFallbackCacheKey(programId) {
|
|
4695
|
-
return
|
|
4813
|
+
return programId;
|
|
4696
4814
|
}
|
|
4697
4815
|
function cacheTtlMsFromDiagnostics(cache) {
|
|
4698
4816
|
return cache.enabled ? cache.ttl_ms : 0;
|
|
@@ -4711,10 +4829,12 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
4711
4829
|
requestId: api.requestId,
|
|
4712
4830
|
upstreamRequestId: api.upstreamRequestId,
|
|
4713
4831
|
fetchedAt: api.fetchedAt,
|
|
4832
|
+
noStore: api.noStore,
|
|
4714
4833
|
cache: readObject(stripUndefined({
|
|
4715
4834
|
hit: api.cached,
|
|
4716
4835
|
coalesced_live_request: api.coalesced,
|
|
4717
|
-
expires_at: api.cacheExpiresAt
|
|
4836
|
+
expires_at: api.cacheExpiresAt,
|
|
4837
|
+
no_store: api.noStore
|
|
4718
4838
|
})),
|
|
4719
4839
|
source: "target_export",
|
|
4720
4840
|
rawTargets,
|
|
@@ -4761,7 +4881,8 @@ async function getProgramFromTargetsOnlyFallback(client, programId, programError
|
|
|
4761
4881
|
cache: readObject(stripUndefined({
|
|
4762
4882
|
hit: targetsApi.cached,
|
|
4763
4883
|
coalesced_live_request: targetsApi.coalesced,
|
|
4764
|
-
expires_at: targetsApi.cacheExpiresAt
|
|
4884
|
+
expires_at: targetsApi.cacheExpiresAt,
|
|
4885
|
+
no_store: targetsApi.noStore
|
|
4765
4886
|
})),
|
|
4766
4887
|
source_requests: [
|
|
4767
4888
|
stripUndefined({
|
|
@@ -4906,8 +5027,6 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4906
5027
|
generated_at: new Date().toISOString()
|
|
4907
5028
|
});
|
|
4908
5029
|
const exportId = randomUUID();
|
|
4909
|
-
const resourceUri = exportResourceUri(exportId);
|
|
4910
|
-
rememberExport(exportStore, exportId, exportPayload);
|
|
4911
5030
|
return stripUndefined({
|
|
4912
5031
|
request_id: randomUUID(),
|
|
4913
5032
|
source_requests: sourceRequests,
|
|
@@ -4915,7 +5034,7 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4915
5034
|
errors: errors.length > 0 ? errors : undefined,
|
|
4916
5035
|
export: {
|
|
4917
5036
|
export_id: exportId,
|
|
4918
|
-
|
|
5037
|
+
data: exportPayload,
|
|
4919
5038
|
preview: previewExportPayload(exportPayload),
|
|
4920
5039
|
returned_preview_count: previewExportCount(exportPayload),
|
|
4921
5040
|
limit: input.limit
|
|
@@ -5642,6 +5761,10 @@ function applyCollectionContinuation(payload, field, label, store, config) {
|
|
|
5642
5761
|
if (!items || items.length <= inlineLimit) {
|
|
5643
5762
|
return payload;
|
|
5644
5763
|
}
|
|
5764
|
+
// A no-store page stays only in this tool result, never in a reusable resource.
|
|
5765
|
+
if (readBoolean(readObject(payload.cache)?.no_store) === true) {
|
|
5766
|
+
return payload;
|
|
5767
|
+
}
|
|
5645
5768
|
const continuationId = randomUUID();
|
|
5646
5769
|
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5647
5770
|
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
@@ -5898,10 +6021,12 @@ function formatProgram(program, mode) {
|
|
|
5898
6021
|
function compactProgram(program) {
|
|
5899
6022
|
return stripUndefined({
|
|
5900
6023
|
id: stringField(program, "id"),
|
|
6024
|
+
catalog_id: stringField(program, "catalog_id"),
|
|
5901
6025
|
handle: stringField(program, "handle"),
|
|
5902
6026
|
name: stringField(program, "name"),
|
|
5903
6027
|
platform: stringField(program, "platform"),
|
|
5904
6028
|
catalog: stringField(program, "catalog"),
|
|
6029
|
+
program_kind: stringField(program, "program_kind"),
|
|
5905
6030
|
program_status: stringField(program, "program_status"),
|
|
5906
6031
|
is_active: readBoolean(program.is_active),
|
|
5907
6032
|
program_type: stringField(program, "program_type"),
|
|
@@ -5910,15 +6035,21 @@ function compactProgram(program) {
|
|
|
5910
6035
|
policy_url: stringField(program, "policy_url"),
|
|
5911
6036
|
reporting_url: stringField(program, "reporting_url"),
|
|
5912
6037
|
reward_status: stringField(program, "reward_status"),
|
|
5913
|
-
data_capabilities:
|
|
5914
|
-
opportunity_tag:
|
|
6038
|
+
data_capabilities: program.data_capabilities,
|
|
6039
|
+
opportunity_tag: program.opportunity_tag,
|
|
5915
6040
|
competition_risk: stringField(program, "competition_risk"),
|
|
5916
6041
|
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5917
|
-
submission_cost:
|
|
5918
|
-
reward_range:
|
|
5919
|
-
|
|
6042
|
+
submission_cost: program.submission_cost,
|
|
6043
|
+
reward_range: program.reward_range,
|
|
6044
|
+
reward_range_label: stringField(program, "reward_range_label"),
|
|
6045
|
+
public_report_count: program.public_report_count,
|
|
5920
6046
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
5921
6047
|
first_seen_date: stringField(program, "first_seen_date"),
|
|
6048
|
+
launch_at: program.launch_at,
|
|
6049
|
+
launch_at_precision: stringField(program, "launch_at_precision"),
|
|
6050
|
+
ends_at: program.ends_at,
|
|
6051
|
+
ends_on: program.ends_on,
|
|
6052
|
+
verification: program.verification,
|
|
5922
6053
|
last_updated: stringField(program, "last_updated"),
|
|
5923
6054
|
targets_updated_at: stringField(program, "targets_updated_at"),
|
|
5924
6055
|
scope_summary: readObject(program.scope_summary),
|
|
@@ -6240,7 +6371,7 @@ function candidateLooksLikeCtf(candidate) {
|
|
|
6240
6371
|
return candidate.looksLikeCtf;
|
|
6241
6372
|
}
|
|
6242
6373
|
function candidateLooksContestLike(candidate) {
|
|
6243
|
-
return
|
|
6374
|
+
return stringField(candidate.program, "program_kind") === "contest";
|
|
6244
6375
|
}
|
|
6245
6376
|
function candidateHasKnownNoBounty(candidate) {
|
|
6246
6377
|
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
@@ -7346,6 +7477,7 @@ async function findPrograms(client, config, input) {
|
|
|
7346
7477
|
const collected = await collectPrograms(client, {
|
|
7347
7478
|
platforms: input.platforms,
|
|
7348
7479
|
tags: upstreamTags,
|
|
7480
|
+
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
|
|
7349
7481
|
updated_since: input.updated_since,
|
|
7350
7482
|
opportunity_levels: input.opportunity_levels,
|
|
7351
7483
|
max_pages: input.max_pages,
|
|
@@ -7394,6 +7526,7 @@ async function findPrograms(client, config, input) {
|
|
|
7394
7526
|
exclude_non_web: input.exclude_non_web,
|
|
7395
7527
|
has_api_scope: input.has_api_scope,
|
|
7396
7528
|
contest_like: input.contest_like ? true : undefined,
|
|
7529
|
+
program_kind: input.program_kind,
|
|
7397
7530
|
vdp_mode: input.vdp_mode && input.vdp_mode !== "include" ? input.vdp_mode : undefined,
|
|
7398
7531
|
fresh_launch_days: input.fresh_launch_days,
|
|
7399
7532
|
min_wildcards: input.min_wildcards,
|
|
@@ -7823,6 +7956,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
|
|
|
7823
7956
|
platforms: options.platforms,
|
|
7824
7957
|
tags: options.tags,
|
|
7825
7958
|
updated_since: options.updated_since,
|
|
7959
|
+
program_kind: source.kind === "programs" ? options.program_kind : undefined,
|
|
7826
7960
|
page,
|
|
7827
7961
|
page_size: MAX_PROGRAMS_PER_SEARCH
|
|
7828
7962
|
});
|
|
@@ -7908,6 +8042,9 @@ function toProgramCandidate(rawProgram, webBaseUrl) {
|
|
|
7908
8042
|
return candidate;
|
|
7909
8043
|
}
|
|
7910
8044
|
function programCandidateMatches(candidate, filters) {
|
|
8045
|
+
if (filters.program_kind && stringField(candidate.program, "program_kind") !== filters.program_kind) {
|
|
8046
|
+
return false;
|
|
8047
|
+
}
|
|
7911
8048
|
const programStatus = normalizeTag(stringField(candidate.program, "program_status") ?? "");
|
|
7912
8049
|
if (readBoolean(candidate.program.is_active) === false || programStatus === "paused") {
|
|
7913
8050
|
return false;
|
|
@@ -8270,7 +8407,7 @@ function registerPrompts(server, profile) {
|
|
|
8270
8407
|
tags: z.string().optional().describe("Optional comma-separated scope/language/target-type tags."),
|
|
8271
8408
|
max_programs: z.string().optional().describe("Optional maximum shortlist size.")
|
|
8272
8409
|
}
|
|
8273
|
-
}, (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.
|
|
8410
|
+
}, (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. Check each result's cache metadata before describing its freshness.
|
|
8274
8411
|
|
|
8275
8412
|
Find the best BBRadar program candidates.
|
|
8276
8413
|
${discoveryToolInstructions}
|
|
@@ -8321,8 +8458,12 @@ The plan must:
|
|
|
8321
8458
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
8322
8459
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
8323
8460
|
}
|
|
8324
|
-
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
8325
|
-
const normalizedFilters = stableStringify(
|
|
8461
|
+
function resolveFeedCursor(store, endpoint, filters, resume, config) {
|
|
8462
|
+
const normalizedFilters = stableStringify({
|
|
8463
|
+
api_base_url: config.apiBaseUrl,
|
|
8464
|
+
credential_scope: credentialCacheScope(config),
|
|
8465
|
+
filters: normalizeFeedCursorFilters(filters)
|
|
8466
|
+
});
|
|
8326
8467
|
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
8327
8468
|
const explicitAfterId = readNumber(filters.after_id);
|
|
8328
8469
|
if (explicitAfterId !== undefined) {
|
|
@@ -8353,7 +8494,7 @@ function advanceFeedCursor(store, cursor, rawMeta) {
|
|
|
8353
8494
|
stored_in_memory: true
|
|
8354
8495
|
};
|
|
8355
8496
|
}
|
|
8356
|
-
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, fetchPage, refreshInventory) {
|
|
8497
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, initialPage, fetchPage, refreshInventory) {
|
|
8357
8498
|
const startedAt = Date.now();
|
|
8358
8499
|
const results = [];
|
|
8359
8500
|
const sourceRequests = [];
|
|
@@ -8370,7 +8511,7 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8370
8511
|
do {
|
|
8371
8512
|
let api;
|
|
8372
8513
|
try {
|
|
8373
|
-
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot);
|
|
8514
|
+
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot, activeSnapshot ? initialPage + pagesFetched : 1);
|
|
8374
8515
|
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8375
8516
|
}
|
|
8376
8517
|
catch (error) {
|
|
@@ -8466,6 +8607,10 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8466
8607
|
}
|
|
8467
8608
|
const data = readObject(api.data);
|
|
8468
8609
|
const returnedSnapshot = readString(readObject(data?.meta)?.snapshot);
|
|
8610
|
+
if (activeSnapshot && !returnedSnapshot)
|
|
8611
|
+
throw new Error("Stable snapshot response omitted its token; discard this page and start a new snapshot.");
|
|
8612
|
+
if (activeSnapshot && activeSnapshot !== "start" && returnedSnapshot !== activeSnapshot)
|
|
8613
|
+
throw new Error("Stable snapshot changed between pages; discard these pages and start a new snapshot.");
|
|
8469
8614
|
if (activeSnapshot === "start" && returnedSnapshot) {
|
|
8470
8615
|
activeSnapshot = returnedSnapshot;
|
|
8471
8616
|
}
|
|
@@ -8668,21 +8813,49 @@ function withApiMetadata(api, payload) {
|
|
|
8668
8813
|
hit: api.cached,
|
|
8669
8814
|
stale_while_revalidate: api.stale,
|
|
8670
8815
|
coalesced_live_request: api.coalesced,
|
|
8671
|
-
expires_at: api.cacheExpiresAt
|
|
8816
|
+
expires_at: api.cacheExpiresAt,
|
|
8817
|
+
no_store: api.noStore
|
|
8672
8818
|
}),
|
|
8673
8819
|
fetched_at: api.fetchedAt,
|
|
8674
8820
|
...payload
|
|
8675
8821
|
});
|
|
8676
8822
|
}
|
|
8823
|
+
function proPage(api, field) {
|
|
8824
|
+
const raw = readObject(api.data);
|
|
8825
|
+
if (!raw || !(field === "activity_summary" ? readObject(raw[field]) : Array.isArray(raw[field]))) {
|
|
8826
|
+
throw new Error(`BBRadar Pro API returned a malformed page: expected ${field}.`);
|
|
8827
|
+
}
|
|
8828
|
+
const data = sanitizeProPayload(api.data);
|
|
8829
|
+
return { ...data, ...withApiMetadata(api, {}) };
|
|
8830
|
+
}
|
|
8831
|
+
function assertProFeature(client, endpoint, flag) {
|
|
8832
|
+
const state = client.capabilitySnapshot();
|
|
8833
|
+
if (state.status !== "negotiated")
|
|
8834
|
+
return;
|
|
8835
|
+
const capability = client.endpointCapability(endpoint);
|
|
8836
|
+
if (!capability)
|
|
8837
|
+
throw new Error(`The negotiated BBRadar API does not advertise ${endpoint}. Upgrade the API deployment to use this tool.`);
|
|
8838
|
+
if (flag && capability[flag] === false) {
|
|
8839
|
+
throw new Error(`${endpoint} is advertised but ${flag}=false on this API deployment.`);
|
|
8840
|
+
}
|
|
8841
|
+
}
|
|
8842
|
+
function assertHistoryCapture(client) {
|
|
8843
|
+
if (client.capabilitySnapshot().status === "negotiated" &&
|
|
8844
|
+
client.endpointCapability("history_changes")?.history_capture_enabled === false) {
|
|
8845
|
+
throw new Error("History endpoints are advertised but history_capture_enabled=false on this API deployment.");
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8677
8848
|
function apiSourceMetadata(api) {
|
|
8678
8849
|
return stripUndefined({
|
|
8679
8850
|
cache_hit: api.cached,
|
|
8680
8851
|
cache_expires_at: api.cacheExpiresAt,
|
|
8681
|
-
coalesced_live_request: api.coalesced
|
|
8852
|
+
coalesced_live_request: api.coalesced,
|
|
8853
|
+
no_store: api.noStore
|
|
8682
8854
|
});
|
|
8683
8855
|
}
|
|
8684
8856
|
function errorPayload(error, decision, toolName, durationMs) {
|
|
8685
8857
|
if (error instanceof BBRadarApiError) {
|
|
8858
|
+
const recovery = error.status === 410 ? recoveryForGone(error, toolName) : undefined;
|
|
8686
8859
|
return stripUndefined({
|
|
8687
8860
|
request_id: error.requestId,
|
|
8688
8861
|
upstream_request_id: error.upstreamRequestId,
|
|
@@ -8692,6 +8865,7 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8692
8865
|
detail: sanitizeJson(error.detail),
|
|
8693
8866
|
errors: sanitizeJson(error.errors)
|
|
8694
8867
|
}),
|
|
8868
|
+
recovery,
|
|
8695
8869
|
mcp_rate_limit: toRateLimitPayload(decision),
|
|
8696
8870
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8697
8871
|
});
|
|
@@ -8705,6 +8879,19 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8705
8879
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8706
8880
|
};
|
|
8707
8881
|
}
|
|
8882
|
+
function recoveryForGone(error, toolName) {
|
|
8883
|
+
const detail = typeof error.detail === "string" ? error.detail.toLowerCase() : error.message.toLowerCase();
|
|
8884
|
+
if (detail.includes("retained analytics") || detail.includes("precede")) {
|
|
8885
|
+
return { action: "choose_supported_interval", message: "Choose dates inside the retained analytics window." };
|
|
8886
|
+
}
|
|
8887
|
+
if (detail.includes("snapshot")) {
|
|
8888
|
+
return { action: "start_new_snapshot", message: "Restart on page 1 with snapshot=start; do not combine pages from the old snapshot." };
|
|
8889
|
+
}
|
|
8890
|
+
if (toolName.startsWith("get_history") || toolName === "get_program_history" || toolName.startsWith("get_analytics")) {
|
|
8891
|
+
return { action: "restart_query", message: "Restart this History or analytics query without its expired opaque cursor, keeping the intended filters." };
|
|
8892
|
+
}
|
|
8893
|
+
return { action: "reconcile_inventory", message: "The retained numeric feed has a gap; reconcile current inventory before seeding a new after_id." };
|
|
8894
|
+
}
|
|
8708
8895
|
function recordToolMetric(toolName, durationMs, status) {
|
|
8709
8896
|
const existing = TOOL_METRICS.get(toolName) ??
|
|
8710
8897
|
{
|
|
@@ -8823,15 +9010,19 @@ function compactTarget(target) {
|
|
|
8823
9010
|
identifier: targetIdentifier(target),
|
|
8824
9011
|
target_type: stringField(target, "target_type"),
|
|
8825
9012
|
in_scope: readBoolean(target.in_scope),
|
|
8826
|
-
eligible_for_bounty:
|
|
9013
|
+
eligible_for_bounty: target.eligible_for_bounty,
|
|
8827
9014
|
wildcard: readBoolean(target.wildcard),
|
|
8828
9015
|
path_kind: stringField(target, "path_kind"),
|
|
8829
9016
|
path_prefix: stringField(target, "path_prefix"),
|
|
8830
9017
|
match_quality: stringField(target, "match_quality"),
|
|
8831
9018
|
match_reason: stringField(target, "match_reason"),
|
|
8832
|
-
target_opportunity_score:
|
|
9019
|
+
target_opportunity_score: target.target_opportunity_score,
|
|
8833
9020
|
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
9021
|
+
score_version: target.score_version,
|
|
9022
|
+
evidence_coverage: target.evidence_coverage,
|
|
9023
|
+
target_score_breakdown: target.target_score_breakdown,
|
|
8834
9024
|
competition_risk: stringField(target, "competition_risk"),
|
|
9025
|
+
competition_risk_reason: stringField(target, "competition_risk_reason"),
|
|
8835
9026
|
why_this_target: stringField(target, "why_this_target")
|
|
8836
9027
|
});
|
|
8837
9028
|
}
|