@bbradar/mcp 0.2.5 → 0.3.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/CHANGELOG.md +28 -0
- package/README.md +233 -10
- package/benchmark/compare-revisions.mjs +164 -0
- package/benchmark/pro-benchmark.mjs +106 -0
- package/dist/activityContract.d.ts +2 -0
- package/dist/activityContract.js +15 -0
- package/dist/activityContract.js.map +1 -0
- package/dist/bbradarClient.d.ts +21 -1
- package/dist/bbradarClient.js +263 -49
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +50 -13
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +853 -103
- package/dist/server.js.map +1 -1
- package/package.json +6 -4
package/dist/server.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { requiresActivityQuery } from "./activityContract.js";
|
|
2
3
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
5
6
|
import { NumericCursorStore } from "./cursorStore.js";
|
|
6
7
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
7
8
|
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.
|
|
9
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeProPayload, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
10
|
+
const SERVER_VERSION = "0.3.0";
|
|
10
11
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
11
12
|
const MAX_TARGET_EXPORT = 500;
|
|
12
13
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -64,7 +65,15 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
64
65
|
"list_filters",
|
|
65
66
|
"get_bbradar_guide",
|
|
66
67
|
"get_mcp_status",
|
|
67
|
-
"export_targets"
|
|
68
|
+
"export_targets",
|
|
69
|
+
"get_history_changes",
|
|
70
|
+
"get_history_change_detail",
|
|
71
|
+
"get_program_history",
|
|
72
|
+
"get_program_activity",
|
|
73
|
+
"get_analytics_platform_series",
|
|
74
|
+
"get_analytics_program_activity",
|
|
75
|
+
"get_analytics_program_detail",
|
|
76
|
+
"get_activity_capabilities", "get_activity_snapshot", "get_activity_rankings", "get_program_payouts", "get_activity_change_detail"
|
|
68
77
|
]);
|
|
69
78
|
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
70
79
|
search_targets: "target_search",
|
|
@@ -97,7 +106,6 @@ const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
|
97
106
|
];
|
|
98
107
|
const PRIMARY_PROGRAM_ID_PLATFORM_GUESSES = PROGRAM_ID_PLATFORM_GUESSES.slice(0, 8);
|
|
99
108
|
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
109
|
const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
|
|
102
110
|
const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
|
|
103
111
|
"program",
|
|
@@ -181,7 +189,7 @@ const changeListModeSchema = z.enum(["compact", "full"]);
|
|
|
181
189
|
const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
|
|
182
190
|
const programListModeSchema = z.enum(["compact", "full"]);
|
|
183
191
|
const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
|
|
184
|
-
const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
|
|
192
|
+
const vdpModeSchema = z.enum(["include", "exclude", "only", "only_likely", "only_known_no_bounty"]);
|
|
185
193
|
const targetMatchModeSchema = z
|
|
186
194
|
.enum(["smart", "contains", "exact", "suffix", "wildcard"])
|
|
187
195
|
.describe("How to match target_query. smart is forgiving for URLs, wildcards, target names, and partial words.");
|
|
@@ -254,6 +262,9 @@ const stableSnapshotSchema = z
|
|
|
254
262
|
message: "Snapshot tokens cannot contain control characters."
|
|
255
263
|
})
|
|
256
264
|
.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.");
|
|
265
|
+
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.");
|
|
266
|
+
const analyticsDateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use an inclusive UTC YYYY-MM-DD date.");
|
|
267
|
+
const proLimitSchema = z.number().int().min(1).max(100);
|
|
257
268
|
const pageSchema = z.number().int().min(1).default(1);
|
|
258
269
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
259
270
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
@@ -274,11 +285,64 @@ const programSortSchema = z
|
|
|
274
285
|
"best_hunt_value"
|
|
275
286
|
])
|
|
276
287
|
.default("best_match");
|
|
288
|
+
const ACTIVITY_TABS = ["payouts", "value", "scope", "rewards", "updates", "reports", "response", "status", "all_changes"];
|
|
289
|
+
const ACTIVITY_EVENT_PERIODS = ["7d", "30d", "90d"];
|
|
290
|
+
const ACTIVITY_COUNT_TABS = new Set(["payouts", "reports", "response"]);
|
|
291
|
+
const ACTIVITY_SHARED_PARAMS = ["tab", "sort", "direction", "platforms", "tags", "program_kind", "program_type", "search",
|
|
292
|
+
"current_status", "labels", "opportunity_tiers", "cursor", "limit", "snapshot"];
|
|
293
|
+
// Inputs the API accepts only for these views. Anything else is rejected before
|
|
294
|
+
// a request so it cannot be silently ignored or reinterpreted.
|
|
295
|
+
const ACTIVITY_TAB_PARAMS = {
|
|
296
|
+
payouts: ["period", "basis", "precision"],
|
|
297
|
+
value: ["include_map"],
|
|
298
|
+
scope: ["period"],
|
|
299
|
+
rewards: ["period"],
|
|
300
|
+
updates: ["period", "category"],
|
|
301
|
+
reports: ["period", "launched_within", "report_change_min", "report_change_max"],
|
|
302
|
+
response: ["period", "launched_within"],
|
|
303
|
+
status: ["period", "change_type"],
|
|
304
|
+
all_changes: ["period", "category"]
|
|
305
|
+
};
|
|
306
|
+
const activityCatalogSortSchema = z.enum(["updated", "payout_amount", "payout_lower_bound", "payout_upper_bound", "scope_added", "scope_net", "reward_change", "active_days", "reports", "reports_change", "resolved", "last_resolved", "first_response", "time_to_bounty", "last_change"]);
|
|
307
|
+
const activityCountPeriodSchema = () => z.enum(["24h", "7d", "30d", "90d", "all_time"]);
|
|
308
|
+
const activityEventPeriodSchema = () => z.enum(["7d", "30d", "90d"]);
|
|
309
|
+
const activityCatalogNumberSchema = () => z.number().finite().min(0).optional();
|
|
310
|
+
const activityCatalogInput = {
|
|
311
|
+
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
312
|
+
payout_metric: z.enum(["payout_total", "payout_increase", "payout_native_90d"]).optional(),
|
|
313
|
+
payout_period: activityCountPeriodSchema().optional(),
|
|
314
|
+
payout_precision: z.enum(["exact", "proven_bounds"]).optional(),
|
|
315
|
+
payout_min: activityCatalogNumberSchema(), payout_max: activityCatalogNumberSchema(),
|
|
316
|
+
payout_positive: z.boolean().optional().describe("true requires a proved positive payout; false requires an exact observed zero. Omit to include either."),
|
|
317
|
+
payout_activity: z.enum(["increase", "no_increase", "lower"]).optional(),
|
|
318
|
+
scope_period: activityEventPeriodSchema().optional(),
|
|
319
|
+
scope_added_min: activityCatalogNumberSchema(), scope_removed_min: activityCatalogNumberSchema(),
|
|
320
|
+
scope_net_min: z.number().finite().optional(), scope_net_max: z.number().finite().optional(),
|
|
321
|
+
reward_period: activityEventPeriodSchema().optional(),
|
|
322
|
+
reward_change: z.enum(["increased", "decreased"]).optional(), reward_change_min_pct: activityCatalogNumberSchema(),
|
|
323
|
+
updates_period: activityEventPeriodSchema().optional(),
|
|
324
|
+
active_days_min: activityCatalogNumberSchema(), changes_min: activityCatalogNumberSchema(),
|
|
325
|
+
change_category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
326
|
+
reports_period: activityCountPeriodSchema().optional(),
|
|
327
|
+
reports_min: activityCatalogNumberSchema(), reports_max: activityCatalogNumberSchema(),
|
|
328
|
+
reports_change: z.enum(["rising", "falling"]).optional(), reports_change_min_pct: activityCatalogNumberSchema(),
|
|
329
|
+
launched_within: z.enum(["7d", "30d", "90d"]).optional(),
|
|
330
|
+
resolved_period: activityCountPeriodSchema().optional(), resolved_min: activityCatalogNumberSchema(),
|
|
331
|
+
last_resolved_within: z.enum(["7d", "30d", "90d"]).optional(),
|
|
332
|
+
first_response_max_hours: activityCatalogNumberSchema(), time_to_bounty_max_hours: activityCatalogNumberSchema(),
|
|
333
|
+
status_period: activityEventPeriodSchema().optional(),
|
|
334
|
+
status_change: z.enum(["first_seen", "paused", "resumed", "left_listing", "returned"]).optional(),
|
|
335
|
+
current_status: z.enum(["active", "paused", "suspended", "disabled", "closed", "unavailable", "unknown"]).optional(),
|
|
336
|
+
activity_labels: z.array(z.string().min(1).max(64)).max(10).optional(),
|
|
337
|
+
sort_by: activityCatalogSortSchema.optional(), sort_direction: z.enum(["asc", "desc"]).optional()
|
|
338
|
+
};
|
|
339
|
+
const activityCatalogGuidance = "Give each Activity signal its period (e.g. payout_period=30d with payout_positive=true; scope_period with scope_added_min) and use sort_by/sort_direction combinations enabled in get_activity_capabilities; default is updated descending. Tracked no_increase/lower require 30d. Nothing falls back to another period or precision.";
|
|
340
|
+
const freeSubmissionOnlySchema = z.boolean().default(false).describe("Only programs with no detected submission fee or deposit.");
|
|
277
341
|
const programIdSchema = z
|
|
278
342
|
.string()
|
|
279
343
|
.trim()
|
|
280
344
|
.min(3)
|
|
281
|
-
.max(
|
|
345
|
+
.max(620)
|
|
282
346
|
.regex(/^[^:/?#\u0000-\u001F\u007F]+:[^?#\u0000-\u001F\u007F]+$/, "Use platform:handle, for example HackerOne:example-handle.")
|
|
283
347
|
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
284
348
|
message: "program_id cannot contain control characters."
|
|
@@ -296,49 +360,24 @@ const programIdentifierSchema = z
|
|
|
296
360
|
function jsonRecordSchema() {
|
|
297
361
|
return z.record(z.unknown());
|
|
298
362
|
}
|
|
299
|
-
function rateLimitOutputSchema() {
|
|
300
|
-
return z.object({
|
|
301
|
-
remaining: z.number().int().min(0),
|
|
302
|
-
reset_at: z.string()
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
function toolTimingOutputSchema() {
|
|
306
|
-
return z.object({
|
|
307
|
-
tool_name: z.string(),
|
|
308
|
-
duration_ms: z.number().int().min(0)
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
function cacheOutputSchema() {
|
|
312
|
-
return z.object({
|
|
313
|
-
hit: z.boolean(),
|
|
314
|
-
stale_while_revalidate: z.boolean().optional(),
|
|
315
|
-
coalesced_live_request: z.boolean().optional(),
|
|
316
|
-
expires_at: z.string().optional()
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
function errorOutputSchema() {
|
|
320
|
-
return z.object({
|
|
321
|
-
message: z.string(),
|
|
322
|
-
status: z.number().int().optional(),
|
|
323
|
-
detail: z.unknown().optional(),
|
|
324
|
-
errors: z.unknown().optional()
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
363
|
function programOutputSchema() {
|
|
328
364
|
return jsonRecordSchema();
|
|
329
365
|
}
|
|
366
|
+
// Every tool repeats this envelope in discovery. Diagnostic objects (cache,
|
|
367
|
+
// rate limit, timing, error) stay open records so each tool's schema stays small.
|
|
330
368
|
function apiEnvelopeOutputShape() {
|
|
331
369
|
return {
|
|
332
370
|
request_id: z.string(),
|
|
333
371
|
upstream_request_id: z.string().optional(),
|
|
334
372
|
fetched_at: z.string().optional(),
|
|
335
|
-
cache:
|
|
373
|
+
cache: jsonRecordSchema().optional(),
|
|
336
374
|
source_requests: z.array(jsonRecordSchema()).optional(),
|
|
337
|
-
mcp_rate_limit:
|
|
338
|
-
mcp_timing:
|
|
375
|
+
mcp_rate_limit: jsonRecordSchema().optional(),
|
|
376
|
+
mcp_timing: jsonRecordSchema().optional(),
|
|
339
377
|
warnings: z.array(z.string()).optional(),
|
|
340
378
|
program_id_resolution: jsonRecordSchema().optional(),
|
|
341
|
-
|
|
379
|
+
recovery: jsonRecordSchema().optional(),
|
|
380
|
+
error: jsonRecordSchema().optional()
|
|
342
381
|
};
|
|
343
382
|
}
|
|
344
383
|
function programListOutputShape() {
|
|
@@ -356,7 +395,8 @@ function findProgramsOutputShape() {
|
|
|
356
395
|
warnings: z.array(z.string()).optional(),
|
|
357
396
|
errors: z.array(jsonRecordSchema()).optional(),
|
|
358
397
|
upstream_budget: jsonRecordSchema().optional(),
|
|
359
|
-
ranking_scope: jsonRecordSchema().optional()
|
|
398
|
+
ranking_scope: jsonRecordSchema().optional(),
|
|
399
|
+
scan: jsonRecordSchema().optional()
|
|
360
400
|
};
|
|
361
401
|
}
|
|
362
402
|
function decisionOutputShape() {
|
|
@@ -400,7 +440,11 @@ const programCandidatePresetInputSchema = {
|
|
|
400
440
|
};
|
|
401
441
|
function createProfiledToolRegistrar(server, profile, client) {
|
|
402
442
|
return (name, toolConfig, callback) => {
|
|
403
|
-
const
|
|
443
|
+
const scopedCallback = ((...args) => {
|
|
444
|
+
const extra = args[args.length - 1];
|
|
445
|
+
return client.withRequestContext(extra.signal, () => callback(...args), readObject(args[0])?.snapshot === "start");
|
|
446
|
+
});
|
|
447
|
+
const registeredTool = server.registerTool(name, toolConfig, scopedCallback);
|
|
404
448
|
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
405
449
|
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
406
450
|
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
@@ -417,6 +461,9 @@ export function createBbradarServer(client, config) {
|
|
|
417
461
|
"Use resolve_program or run_program_name_action for a named program.",
|
|
418
462
|
"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
463
|
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
464
|
+
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities, then get_activity_rankings with the matching tab for one ranking. For several conditions at once (e.g. recent payouts plus new scope, Web2-only, low competition, no submission fee) use one find_hunt_targets or find_programs call with Activity inputs and local filters together. Keep periods explicit; never intersect separately paginated rankings.",
|
|
465
|
+
"Activity data is published in versions. get_activity_snapshot reports the current version; pass its snapshot_id as snapshot to bind capabilities and first pages to one version, and never mix rows from different versions.",
|
|
466
|
+
"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
467
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
421
468
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
422
469
|
]
|
|
@@ -426,12 +473,15 @@ export function createBbradarServer(client, config) {
|
|
|
426
473
|
"Simple default path: find_hunt_targets once for ranked target/program discovery, run_program_name_action for one named program, get_program_eligible_targets_by_name for exhaustive target lists by program name.",
|
|
427
474
|
"Do not repeat program discovery with alternate sorts or fan out get_program_targets across a shortlist; find_hunt_targets already includes exact bounded samples and recent target additions.",
|
|
428
475
|
"Use advanced tools only after the user's goal is clear or the guide points to them.",
|
|
476
|
+
"For payouts, reports, response measures or Activity comparisons, first read get_activity_capabilities. Use get_activity_rankings (or its per-view alias) for one ranking, or one find_hunt_targets/find_programs call combining Activity inputs with Web2, scope, competition and fee filters. Keep server order and explicit periods; never re-rank a sampled catalog or intersect separate pages.",
|
|
477
|
+
"Activity data is published in versions. get_activity_snapshot reports the current version; pass its snapshot_id as snapshot to bind capabilities and first pages to one version, and never mix rows from different versions.",
|
|
429
478
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
430
479
|
"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.",
|
|
431
480
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
432
481
|
"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
482
|
"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
483
|
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
484
|
+
"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
485
|
"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
486
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
437
487
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -459,11 +509,217 @@ export function createBbradarServer(client, config) {
|
|
|
459
509
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
460
510
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
461
511
|
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
512
|
+
const historyOutput = { ...apiEnvelopeOutputShape(), changes: z.array(jsonRecordSchema()).optional(), coverage: jsonRecordSchema().optional(), meta: jsonRecordSchema().optional() };
|
|
513
|
+
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() };
|
|
514
|
+
// Published Activity versions are 64 lowercase hex characters. "start" and
|
|
515
|
+
// catalog snapshot tokens are different contracts and are rejected here.
|
|
516
|
+
const activitySnapshotSchema = z.string().regex(/^[0-9a-f]{64}$/, "Use a snapshot_id returned by Activity.")
|
|
517
|
+
.describe("Published Activity version (get_activity_snapshot or meta.snapshot_id). Binds reads to one version; expired or withdrawn versions return 410. Never mix with a cursor from another version.");
|
|
518
|
+
const includeMapSchema = z.boolean().default(false)
|
|
519
|
+
.describe("Return Value map points (up to thousands). Off by default: map_points_omitted reports the count.");
|
|
520
|
+
const activityParams = {
|
|
521
|
+
tab: z.enum(ACTIVITY_TABS),
|
|
522
|
+
period: z.enum(["24h", "7d", "30d", "90d", "all_time"]).optional(),
|
|
523
|
+
basis: z.enum(["reported", "tracked"]).optional(),
|
|
524
|
+
precision: z.enum(["exact", "proven_bounds"]).optional(),
|
|
525
|
+
sort: z.string().min(1).max(64).optional(),
|
|
526
|
+
direction: z.enum(["asc", "desc"]).optional(),
|
|
527
|
+
platforms: platformListSchema(), tags: tagListSchema(),
|
|
528
|
+
program_kind: z.enum(["bounty", "contest", "audit", "challenge"]).optional(),
|
|
529
|
+
program_type: z.enum(["paid", "vdp", "unknown"]).optional(),
|
|
530
|
+
search: z.string().min(1).max(200).optional(),
|
|
531
|
+
current_status: z.enum(["active", "paused", "suspended", "disabled", "closed", "unavailable", "unknown"]).optional(),
|
|
532
|
+
labels: z.array(z.string().min(1).max(64)).max(10).optional().describe("Only labels listed by get_activity_capabilities; others return 422."),
|
|
533
|
+
opportunity_tiers: z.array(opportunityLevelSchema).min(1).max(4).optional().describe("Any of these catalog opportunity tiers, as frozen into the Activity version. get_activity_capabilities lists the tiers present in facets.opportunity_tiers; an API without the filter is rejected before any request."),
|
|
534
|
+
launched_within: z.enum(["7d", "30d", "90d"]).optional(),
|
|
535
|
+
category: z.enum(["scope", "rewards", "rules", "program"]).optional(),
|
|
536
|
+
change_type: z.enum(["first_seen", "paused", "resumed", "left_listing", "returned"]).optional(),
|
|
537
|
+
report_change_min: z.number().finite().optional(), report_change_max: z.number().finite().optional(),
|
|
538
|
+
include_map: includeMapSchema,
|
|
539
|
+
cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(25),
|
|
540
|
+
snapshot: activitySnapshotSchema.optional()
|
|
541
|
+
};
|
|
542
|
+
const activityOutput = {
|
|
543
|
+
...apiEnvelopeOutputShape(), rows: z.array(jsonRecordSchema()).optional(), points: z.array(jsonRecordSchema()).optional(),
|
|
544
|
+
tab: z.string().optional(), period: z.string().nullable().optional(),
|
|
545
|
+
basis: z.string().nullable().optional(), precision: z.string().nullable().optional(),
|
|
546
|
+
sort: z.string().nullable().optional(), direction: z.string().optional(), meta: jsonRecordSchema().optional(),
|
|
547
|
+
cohort_median: jsonRecordSchema().nullable().optional(), available_views: z.array(jsonRecordSchema()).optional(),
|
|
548
|
+
map_points_omitted: z.number().int().optional()
|
|
549
|
+
};
|
|
550
|
+
registerTool("get_activity_capabilities", {
|
|
551
|
+
title: "Get Available Activity Views",
|
|
552
|
+
description: "Read the exact enabled period, sort, direction, basis and precision combinations before choosing an Activity ranking. Availability is evaluated before user filters; do not infer combinations from independent enums. Honor each view's sort_directions and default_direction and the enabled label list. The snapshot block identifies the published version these capabilities belong to.",
|
|
553
|
+
inputSchema: z.object({ snapshot: activitySnapshotSchema.optional() }).strict(),
|
|
554
|
+
outputSchema: { ...apiEnvelopeOutputShape(), tabs: z.array(jsonRecordSchema()).optional(), version: z.union([z.string(), z.number()]).optional(), facets: jsonRecordSchema().optional(), labels: z.array(z.string()).optional(), listing: jsonRecordSchema().optional(), listing_payouts_enabled: z.boolean().optional(), derived_signals_enabled: z.boolean().optional(), snapshot: jsonRecordSchema().optional() },
|
|
555
|
+
annotations: readOnlyAnnotations
|
|
556
|
+
}, (args) => runTool("get_activity_capabilities", config, rateLimiter, async () => {
|
|
557
|
+
assertProFeature(client, "activity_capabilities", "enabled");
|
|
558
|
+
assertActivitySnapshotSelector(client, "activity_capabilities", args.snapshot);
|
|
559
|
+
return proPage(await client.getActivityCapabilities(toQuery({ snapshot: args.snapshot })), "tabs");
|
|
560
|
+
}));
|
|
561
|
+
registerTool("get_activity_snapshot", {
|
|
562
|
+
title: "Get Current Activity Version",
|
|
563
|
+
description: "Read only the identity of the currently published Activity version: snapshot_id, frozen as_of cutoff, expires_at, policy_version and suggested poll_seconds. Use it to detect newer Activity data and pass snapshot_id as snapshot to bind capabilities, first pages and payouts to one version. as_of is when BBRadar froze its calculation, not a provider payment or update time.",
|
|
564
|
+
inputSchema: z.object({}).strict(),
|
|
565
|
+
outputSchema: { ...apiEnvelopeOutputShape(), snapshot_id: z.string().optional(), as_of: z.string().optional(), expires_at: z.string().optional(), policy_version: z.string().optional(), poll_seconds: z.number().optional() },
|
|
566
|
+
annotations: readOnlyAnnotations
|
|
567
|
+
}, () => runTool("get_activity_snapshot", config, rateLimiter, async () => {
|
|
568
|
+
assertProFeature(client, "activity_snapshot", "enabled");
|
|
569
|
+
const api = await client.getActivitySnapshot();
|
|
570
|
+
const data = readObject(api.data);
|
|
571
|
+
if (!data || typeof data.snapshot_id !== "string" || !/^[0-9a-f]{64}$/.test(data.snapshot_id)) {
|
|
572
|
+
throw new Error("Malformed Activity version response.");
|
|
573
|
+
}
|
|
574
|
+
return withApiMetadata(api, sanitizeProPayload(api.data));
|
|
575
|
+
}));
|
|
576
|
+
registerTool("get_program_payouts", {
|
|
577
|
+
title: "Get Program Payouts and Response Facts",
|
|
578
|
+
description: "Read reported totals, measured counter increases, source dates, precision, report and response facts, derived signals and label explanations for one exact platform:handle. Keep null separate from zero, bounds separate from exact amounts and unknown payment dates null. Payout timestamps are when increases were seen, never payment dates. No provider requests are made.",
|
|
579
|
+
inputSchema: z.object({ program_id: programIdSchema, snapshot: activitySnapshotSchema.optional(), include_map: includeMapSchema }).strict(),
|
|
580
|
+
// Mirrors the API response, including its version identity: a client that
|
|
581
|
+
// discovered this schema validates every structured result against it.
|
|
582
|
+
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), program: jsonRecordSchema().optional(), summary: jsonRecordSchema().optional(), as_of: z.string().optional(), snapshot_id: z.string().optional(), expires_at: z.string().nullable().optional(), facts: z.array(jsonRecordSchema()).optional(), windows: jsonRecordSchema().optional(), series: z.array(jsonRecordSchema()).optional(), payout_summary: jsonRecordSchema().nullable().optional(), views: z.array(jsonRecordSchema()).optional(), reports: jsonRecordSchema().optional(), response: jsonRecordSchema().optional(), last_increase_at: z.string().nullable().optional(), signals: jsonRecordSchema().optional(), label_details: z.array(jsonRecordSchema()).optional(), labels: z.array(z.string()).optional(), value_points: z.array(jsonRecordSchema()).optional(), map_points_omitted: z.number().int().optional(), meta: jsonRecordSchema().optional() },
|
|
583
|
+
annotations: readOnlyAnnotations
|
|
584
|
+
}, (args) => runTool("get_program_payouts", config, rateLimiter, async () => {
|
|
585
|
+
assertProFeature(client, "program_payouts", "enabled");
|
|
586
|
+
assertActivitySnapshotSelector(client, "program_payouts", args.snapshot);
|
|
587
|
+
const api = await client.getProgramPayouts(args.program_id, toQuery({ snapshot: args.snapshot }));
|
|
588
|
+
if (!readObject(api.data))
|
|
589
|
+
throw new Error("Malformed program payout response.");
|
|
590
|
+
return omitMapPoints(withApiMetadata(api, sanitizeProPayload(api.data)), "value_points", args.include_map);
|
|
591
|
+
}));
|
|
592
|
+
const readActivityRanking = async (tab, args) => {
|
|
593
|
+
assertProFeature(client, "activity_rankings", "enabled");
|
|
594
|
+
assertActivitySnapshotSelector(client, "activity_rankings", args.snapshot);
|
|
595
|
+
assertActivityTierFilter(client, args.opportunity_tiers);
|
|
596
|
+
const { include_map, ...query } = args;
|
|
597
|
+
return activityRankingPage(await client.getActivityRankings(toQuery({ ...query, tab })), include_map === true);
|
|
598
|
+
};
|
|
599
|
+
registerTool("get_activity_rankings", {
|
|
600
|
+
title: "Get Activity Ranking",
|
|
601
|
+
description: "One globally ranked server page of an Activity view, frozen at meta.as_of. Read get_activity_capabilities first: explicit period/basis/precision/sort/direction never fall back, and each tab only accepts its own inputs. tab: payouts (reported and tracked bases stay separate; proven_bounds desc = highest reported minimum, asc = lowest reported maximum, never an exact ranking; source totals can lag); value (fixed lifetime generosity and 90-day volume percentiles, no period; one label gives its shortlist); scope (recorded additions/removals/net, not inventory differences); rewards (net reward-range change, unknown % stays null); updates (active UTC days, change groups); reports (received counts; volume is not competition; all_time is the lifetime counter); response (resolved counts, last resolution, response time; time to bounty is award timing); status (first_seen/paused/resumed/left_listing/returned events); all_changes (chronological events; get_activity_change_detail for before/after). Event tabs use rolling 7d/30d/90d, not calendar months. launched_within applies to reports/response. Payout increases are observed counters, not payment dates. Reuse cursors only with identical inputs. For several signals at once, or with Web2/scope/competition/fee filters, use find_programs or find_hunt_targets.",
|
|
602
|
+
inputSchema: z.object(activityParams).strict(),
|
|
603
|
+
outputSchema: activityOutput,
|
|
604
|
+
annotations: readOnlyAnnotations
|
|
605
|
+
}, (args) => runTool("get_activity_rankings", config, rateLimiter, async () => {
|
|
606
|
+
assertActivityTabInputs(args.tab, args);
|
|
607
|
+
return readActivityRanking(args.tab, args);
|
|
608
|
+
}));
|
|
609
|
+
// Single-view aliases for the full profile; each schema exposes only the
|
|
610
|
+
// inputs its view accepts and shares the ranking tool's request path.
|
|
611
|
+
const activityDescriptions = {
|
|
612
|
+
value: "Discover programs with frozen within-platform value and report-volume percentiles. Generosity is lifetime; volume is 90 days. No period or launch-cohort input applies. Use a label-specific shortlist: Pays near top orders by realization, No payouts seen by received_90d (raw count with its basis; small cohorts remain eligible). Comparative ratio sorts use server percentiles; never recompute after filtering. Preserve label_details explanations and destinations. A counter increase is observed activity, not an individual payment. Map points are returned only with include_map.",
|
|
613
|
+
payouts: "Rank payouts using the same server calculation as BBRadar. Last 7 days and last 30 days are rolling windows. Explicit periods never fall back. For high or low payouts lately, use the same enabled tracked-30-day cohort, or separately label a native-90-day cohort. Reported and tracked bases stay separate. With proven_bounds, descending is Highest reported minimum using lower bounds; ascending is Lowest reported maximum using upper bounds. Neither claims an exact ranking. Source totals can be delayed, including Immunefi.",
|
|
614
|
+
scope: "Rank recorded scope additions, removals, net movement or largest update; not differences between inventory totals. Periods are rolling 7, 30 or 90 days, not calendar months.",
|
|
615
|
+
rewards: "Rank comparable net reward-range changes over rolling 7, 30 or 90 days. Unknown change percentages remain null.",
|
|
616
|
+
updates: "Rank active UTC days and recorded change groups over rolling periods. Baselines and internal corrections are excluded.",
|
|
617
|
+
reports: "Rank received report counts. Each program uses measured lifetime-counter growth where available, otherwise its supported native period. All-time is the current lifetime counter. Previous and change compare like-for-like. Report volume is not competition. Launch cohorts use the canonical launch date; amounts are not reports since launch.",
|
|
618
|
+
response: "Rank resolved counter growth or current lifetime counters, last resolution, response duration or time to bounty. Keep the provider's definition: time to bounty is award timing, not payment settlement. Period applies only to resolved counts.",
|
|
619
|
+
status: "Read distinct First seen, Paused, Resumed, Left listing and Returned events. Event type and current status are separate filters; never merge events from one program.",
|
|
620
|
+
all_changes: "Read chronological semantic program changes with event IDs. Fetch get_activity_change_detail lazily for before/after items, including eligible events for programs that left the catalog. Separate events stay separate."
|
|
621
|
+
};
|
|
622
|
+
for (const tab of ACTIVITY_TABS) {
|
|
623
|
+
const name = `get_activity_${tab === "all_changes" ? "changes" : tab}`;
|
|
624
|
+
const allowed = new Set([...ACTIVITY_SHARED_PARAMS, ...ACTIVITY_TAB_PARAMS[tab]]);
|
|
625
|
+
const shape = Object.fromEntries(Object.entries(activityParams).filter(([key]) => key !== "tab" && allowed.has(key)));
|
|
626
|
+
if (!ACTIVITY_COUNT_TABS.has(tab) && shape.period)
|
|
627
|
+
shape.period = z.enum(ACTIVITY_EVENT_PERIODS).optional();
|
|
628
|
+
registerTool(name, {
|
|
629
|
+
title: `Get Activity ${tab === "all_changes" ? "Changes" : tab[0].toUpperCase() + tab.slice(1)}`,
|
|
630
|
+
description: `${activityDescriptions[tab]} One globally ranked server page, with a frozen as-of cutoff. Reuse cursors only with unchanged filters. Use get_activity_capabilities for supported combinations.`,
|
|
631
|
+
inputSchema: z.object(shape).strict(), outputSchema: activityOutput, annotations: readOnlyAnnotations
|
|
632
|
+
}, (args) => runTool(name, config, rateLimiter, () => readActivityRanking(tab, args)));
|
|
633
|
+
}
|
|
634
|
+
registerTool("get_activity_change_detail", {
|
|
635
|
+
title: "Get Activity Change Detail",
|
|
636
|
+
description: "Read one bounded page of recorded before/after items for an exact Activity event ID in its source snapshot. Pass meta.snapshot_id from the Activity page. Retained eligible events may describe programs that have left the catalog. Reuse an opaque cursor only with the same event, snapshot and limit.",
|
|
637
|
+
inputSchema: z.object({ event_id: z.string().min(1).max(128), snapshot: activitySnapshotSchema.describe("meta.snapshot_id from the Activity page that supplied this event."), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) }).strict(),
|
|
638
|
+
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
639
|
+
}, (args) => runTool("get_activity_change_detail", config, rateLimiter, async () => {
|
|
640
|
+
assertProFeature(client, "activity_event_detail", "enabled");
|
|
641
|
+
return proPage(await client.getActivityChangeDetail(args.event_id, toQuery({ snapshot: args.snapshot, cursor: args.cursor, limit: args.limit })), "items");
|
|
642
|
+
}));
|
|
643
|
+
registerTool("get_history_changes", {
|
|
644
|
+
title: "Get Program History Changes",
|
|
645
|
+
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.",
|
|
646
|
+
inputSchema: {
|
|
647
|
+
cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90),
|
|
648
|
+
platform: z.string().min(1).max(100).optional(), program_id: programIdSchema.optional(),
|
|
649
|
+
category: z.enum(["scope", "rewards", "rules", "program", "repositories"]).optional(),
|
|
650
|
+
search: z.string().min(1).max(120).optional(), limit: proLimitSchema.default(50)
|
|
651
|
+
}, outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
652
|
+
}, (args) => runTool("get_history_changes", config, rateLimiter, async () => {
|
|
653
|
+
assertProFeature(client, "history_changes", "history_capture_enabled");
|
|
654
|
+
if (args.days === 0 && !args.program_id)
|
|
655
|
+
throw new Error("days=0 requires program_id; use get_program_history for Self-Hosted catalog IDs.");
|
|
656
|
+
const api = await client.getHistoryChanges(toQuery(args));
|
|
657
|
+
return proPage(api, "changes");
|
|
658
|
+
}));
|
|
659
|
+
registerTool("get_history_change_detail", {
|
|
660
|
+
title: "Get History Change Detail",
|
|
661
|
+
description: "Fetch one semantic History change's paginated before/after items only when needed.",
|
|
662
|
+
inputSchema: { change_id: z.string().min(1).max(128), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(50) },
|
|
663
|
+
outputSchema: { ...apiEnvelopeOutputShape(), change: jsonRecordSchema().optional(), items: z.array(jsonRecordSchema()).optional(), meta: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
664
|
+
}, (args) => runTool("get_history_change_detail", config, rateLimiter, async () => {
|
|
665
|
+
assertProFeature(client, "history_change_detail");
|
|
666
|
+
assertHistoryCapture(client);
|
|
667
|
+
return proPage(await client.getHistoryChangeDetail(args.change_id, toQuery({ cursor: args.cursor, limit: args.limit })), "items");
|
|
668
|
+
}));
|
|
669
|
+
registerTool("get_program_history", {
|
|
670
|
+
title: "Get Program History",
|
|
671
|
+
description: "One page of exact semantic History for a platform:handle or Self-Hosted:slug catalog ID. Preserve archive id and catalog_id separately.",
|
|
672
|
+
inputSchema: { program_id: programIdSchema, cursor: opaqueCursorSchema.optional(), days: z.number().int().min(0).max(3650).default(90), limit: proLimitSchema.default(50) },
|
|
673
|
+
outputSchema: historyOutput, annotations: readOnlyAnnotations
|
|
674
|
+
}, (args) => runTool("get_program_history", config, rateLimiter, async () => {
|
|
675
|
+
assertProFeature(client, "program_history");
|
|
676
|
+
assertHistoryCapture(client);
|
|
677
|
+
return proPage(await client.getProgramHistory(args.program_id, toQuery({ cursor: args.cursor, days: args.days, limit: args.limit })), "changes");
|
|
678
|
+
}));
|
|
679
|
+
registerTool("get_program_activity", {
|
|
680
|
+
title: "Get Exact Program Activity",
|
|
681
|
+
description: "Read the authoritative activity_summary for one exact program ID.",
|
|
682
|
+
inputSchema: { program_id: programIdSchema },
|
|
683
|
+
outputSchema: { ...apiEnvelopeOutputShape(), program_id: z.string().optional(), activity_summary: jsonRecordSchema().optional() }, annotations: readOnlyAnnotations
|
|
684
|
+
}, (args) => runTool("get_program_activity", config, rateLimiter, async () => {
|
|
685
|
+
assertProFeature(client, "program_activity");
|
|
686
|
+
return proPage(await client.getProgramActivity(args.program_id), "activity_summary");
|
|
687
|
+
}));
|
|
688
|
+
registerTool("get_analytics_platform_series", {
|
|
689
|
+
title: "Get Platform Daily Analytics",
|
|
690
|
+
description: "One page of inclusive UTC daily platform analytics; inspect generation, projection_pending, and tracking_since.",
|
|
691
|
+
inputSchema: { platform: z.string().min(1).max(100), start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
692
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
693
|
+
}, (args) => runTool("get_analytics_platform_series", config, rateLimiter, async () => {
|
|
694
|
+
assertProFeature(client, "analytics_platform_series", "enabled");
|
|
695
|
+
const { platform, ...query } = args;
|
|
696
|
+
return proPage(await client.getAnalyticsPlatformSeries(platform, toQuery(query)), "series");
|
|
697
|
+
}));
|
|
698
|
+
registerTool("get_analytics_program_activity", {
|
|
699
|
+
title: "Rank Programs by Recorded Activity",
|
|
700
|
+
description: "Server-side daily activity ranking. One page; semantic activity excludes baselines and corrections.",
|
|
701
|
+
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) },
|
|
702
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
703
|
+
}, (args) => runTool("get_analytics_program_activity", config, rateLimiter, async () => {
|
|
704
|
+
assertProFeature(client, "analytics_program_activity", "enabled");
|
|
705
|
+
return proPage(await client.getAnalyticsProgramActivity(toQuery(args)), "programs");
|
|
706
|
+
}));
|
|
707
|
+
registerTool("get_analytics_program_detail", {
|
|
708
|
+
title: "Get Program Daily Analytics",
|
|
709
|
+
description: "One page of program daily metrics, carried inventory state, report observations, and source coverage.",
|
|
710
|
+
inputSchema: { program_id: programIdSchema, start: analyticsDateSchema().optional(), end: analyticsDateSchema().optional(), cursor: opaqueCursorSchema.optional(), limit: proLimitSchema.default(100) },
|
|
711
|
+
outputSchema: analyticsOutput, annotations: readOnlyAnnotations
|
|
712
|
+
}, (args) => runTool("get_analytics_program_detail", config, rateLimiter, async () => {
|
|
713
|
+
assertProFeature(client, "analytics_program_detail", "enabled");
|
|
714
|
+
const { program_id, ...query } = args;
|
|
715
|
+
return proPage(await client.getAnalyticsProgramDetail(program_id, toQuery(query)), "series");
|
|
716
|
+
}));
|
|
462
717
|
registerTool("search_programs", {
|
|
463
718
|
title: "Search BBRadar Programs",
|
|
464
|
-
description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in
|
|
465
|
-
inputSchema: {
|
|
719
|
+
description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in. For payout, report, scope-change or other Activity filters use find_programs.`,
|
|
720
|
+
inputSchema: z.object({
|
|
466
721
|
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
722
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional(),
|
|
467
723
|
platforms: platformListSchema(),
|
|
468
724
|
tags: tagListSchema(),
|
|
469
725
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
@@ -471,7 +727,7 @@ export function createBbradarServer(client, config) {
|
|
|
471
727
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
472
728
|
page: pageSchema,
|
|
473
729
|
page_size: programsPageSizeSchema
|
|
474
|
-
},
|
|
730
|
+
}).strict(),
|
|
475
731
|
outputSchema: programListOutputShape(),
|
|
476
732
|
annotations: readOnlyAnnotations
|
|
477
733
|
}, (args) => runTool("search_programs", config, rateLimiter, async () => {
|
|
@@ -718,16 +974,16 @@ export function createBbradarServer(client, config) {
|
|
|
718
974
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
719
975
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
720
976
|
assertStableSnapshotSupported(client, "target_changes", filters.snapshot);
|
|
721
|
-
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
977
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor, config);
|
|
722
978
|
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({
|
|
979
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRecentChanges(toQuery({
|
|
724
980
|
...upstreamFilters,
|
|
725
981
|
minTargetScore: min_target_score,
|
|
726
982
|
targetOpportunityLabel: target_opportunity_label,
|
|
727
983
|
competitionRisk: competition_risk,
|
|
728
984
|
snapshot: activeSnapshot,
|
|
729
985
|
after_id: afterId,
|
|
730
|
-
page: afterId === undefined ? filters.page : 1
|
|
986
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
731
987
|
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.searchTargets(toQuery({
|
|
732
988
|
q: upstreamFilters.search,
|
|
733
989
|
platforms: upstreamFilters.platforms,
|
|
@@ -795,13 +1051,13 @@ export function createBbradarServer(client, config) {
|
|
|
795
1051
|
}
|
|
796
1052
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
797
1053
|
assertStableSnapshotSupported(client, "repository_changes", filters.snapshot);
|
|
798
|
-
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
1054
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor, config);
|
|
799
1055
|
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({
|
|
1056
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, filters.page, (afterId, recoveryProbe, activeSnapshot, pageNumber) => client.getRepositoryChanges(toQuery({
|
|
801
1057
|
...upstreamFilters,
|
|
802
1058
|
snapshot: activeSnapshot,
|
|
803
1059
|
after_id: afterId,
|
|
804
|
-
page: afterId === undefined ? filters.page : 1
|
|
1060
|
+
page: activeSnapshot ? pageNumber : afterId === undefined ? filters.page : 1
|
|
805
1061
|
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.getRepositoryChanges(toQuery({
|
|
806
1062
|
...upstreamFilters,
|
|
807
1063
|
after_id: undefined,
|
|
@@ -830,8 +1086,9 @@ export function createBbradarServer(client, config) {
|
|
|
830
1086
|
}));
|
|
831
1087
|
registerTool("find_hunt_targets", {
|
|
832
1088
|
title: "Find Best Hunt Targets",
|
|
833
|
-
description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, and recently added targets. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Self-Hosted is supported through current inventory and automatically skips its unavailable change feed. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
|
|
834
|
-
inputSchema: {
|
|
1089
|
+
description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, recent payouts or scope changes, and recently added targets. Activity inputs (e.g. payout_period=30d with payout_positive=true, scope_period=30d with scope_added_min=1) first narrow candidates on the server in global order; the other filters and ranking_mode then apply locally. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Self-Hosted is supported through current inventory and automatically skips its unavailable change feed. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
|
|
1090
|
+
inputSchema: z.object({
|
|
1091
|
+
...activityCatalogInput,
|
|
835
1092
|
platforms: platformListSchema(),
|
|
836
1093
|
exclude_platforms: stringListSchema("Platforms to exclude from the final shortlist."),
|
|
837
1094
|
tags: tagListSchema(),
|
|
@@ -858,9 +1115,10 @@ export function createBbradarServer(client, config) {
|
|
|
858
1115
|
max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(DEFAULT_HUNT_PROGRAM_PAGES),
|
|
859
1116
|
max_results: z.number().int().min(1).max(MAX_HUNT_RESULTS).default(10),
|
|
860
1117
|
targets_per_result: z.number().int().min(1).max(MAX_HUNT_TARGETS_PER_RESULT).default(5),
|
|
1118
|
+
free_submission_only: freeSubmissionOnlySchema,
|
|
861
1119
|
ranking_mode: z.enum(["balanced", "highest_reward", "widest_scope", "freshest_targets", "lowest_competition"]).default("balanced"),
|
|
862
1120
|
upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
863
|
-
},
|
|
1121
|
+
}).strict(),
|
|
864
1122
|
outputSchema: {
|
|
865
1123
|
...apiEnvelopeOutputShape(),
|
|
866
1124
|
matches: z.array(jsonRecordSchema()).optional(),
|
|
@@ -868,11 +1126,15 @@ export function createBbradarServer(client, config) {
|
|
|
868
1126
|
errors: z.array(jsonRecordSchema()).optional(),
|
|
869
1127
|
upstream_budget: jsonRecordSchema().optional(),
|
|
870
1128
|
ranking_scope: jsonRecordSchema().optional(),
|
|
1129
|
+
scan: jsonRecordSchema().optional(),
|
|
871
1130
|
result_guidance: jsonRecordSchema().optional(),
|
|
872
1131
|
meta: jsonRecordSchema().optional()
|
|
873
1132
|
},
|
|
874
1133
|
annotations: readOnlyAnnotations
|
|
875
|
-
}, (args) => runTool("find_hunt_targets", config, rateLimiter, async () =>
|
|
1134
|
+
}, (args) => runTool("find_hunt_targets", config, rateLimiter, async () => {
|
|
1135
|
+
const activity = Object.fromEntries(Object.keys(activityCatalogInput).map((key) => [key, args[key]]));
|
|
1136
|
+
return findHuntTargets(client, config, args, activity);
|
|
1137
|
+
}));
|
|
876
1138
|
registerTool("find_repository_hunt_targets", {
|
|
877
1139
|
title: "Find Repository-Aware Hunt Targets",
|
|
878
1140
|
description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary supported platform catalogs, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria. Self-Hosted has no repository stream and is rejected without upstream work.",
|
|
@@ -1090,16 +1352,19 @@ export function createBbradarServer(client, config) {
|
|
|
1090
1352
|
}, (args) => runTool("check_watchlist_new_targets", config, rateLimiter, async () => checkWatchlistNewTargets(client, config, args)));
|
|
1091
1353
|
registerTool("get_opportunities", {
|
|
1092
1354
|
title: "Get BBRadar Opportunities",
|
|
1093
|
-
description: `List programs in one opportunity tier. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
|
|
1094
|
-
inputSchema: {
|
|
1355
|
+
description: `List programs in one opportunity tier. page_size max ${MAX_PROGRAMS_PER_SEARCH}. For Activity filters within a tier use find_programs with one opportunity_levels entry.`,
|
|
1356
|
+
inputSchema: z.object({
|
|
1095
1357
|
level: opportunityLevelSchema.default("elite"),
|
|
1358
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional(),
|
|
1359
|
+
search: searchTextSchema.optional(),
|
|
1096
1360
|
platforms: platformListSchema(),
|
|
1097
1361
|
tags: tagListSchema(),
|
|
1098
1362
|
updated_since: isoDateTimeSchema.optional(),
|
|
1363
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
1099
1364
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
1100
1365
|
page: pageSchema,
|
|
1101
1366
|
page_size: programsPageSizeSchema
|
|
1102
|
-
},
|
|
1367
|
+
}).strict(),
|
|
1103
1368
|
outputSchema: {
|
|
1104
1369
|
...programListOutputShape(),
|
|
1105
1370
|
level: opportunityLevelSchema.optional()
|
|
@@ -1107,11 +1372,15 @@ export function createBbradarServer(client, config) {
|
|
|
1107
1372
|
annotations: readOnlyAnnotations
|
|
1108
1373
|
}, (args) => runTool("get_opportunities", config, rateLimiter, async () => {
|
|
1109
1374
|
const { level, vdp_mode, ...filters } = args;
|
|
1110
|
-
|
|
1375
|
+
assertStableSnapshotSupported(client, "opportunities", filters.snapshot);
|
|
1376
|
+
const combined = args.program_kind !== undefined;
|
|
1377
|
+
if (combined)
|
|
1378
|
+
assertCatalogQuerySupported(client, "opportunities", args);
|
|
1379
|
+
const api = await client.getOpportunities(level, toQuery({ ...filters, vdp_mode: combined ? vdp_mode : undefined }));
|
|
1111
1380
|
const data = readObject(api.data);
|
|
1112
1381
|
const programs = readArray(data?.programs)
|
|
1113
1382
|
.map((program) => toProgramCandidate(program, config.webBaseUrl))
|
|
1114
|
-
.filter((candidate) => candidateMatchesVdpMode(candidate, vdp_mode))
|
|
1383
|
+
.filter((candidate) => combined || candidateMatchesVdpMode(candidate, vdp_mode))
|
|
1115
1384
|
.map((candidate) => candidate.program);
|
|
1116
1385
|
return withApiMetadata(api, {
|
|
1117
1386
|
level,
|
|
@@ -1121,8 +1390,12 @@ export function createBbradarServer(client, config) {
|
|
|
1121
1390
|
}));
|
|
1122
1391
|
registerTool("find_programs", {
|
|
1123
1392
|
title: "Find BBRadar Programs",
|
|
1124
|
-
description:
|
|
1125
|
-
inputSchema: {
|
|
1393
|
+
description: `Program discovery. Activity inputs (payouts, reports, scope/reward changes, response, status, labels) run on the server over the complete catalog in one global order, within at most one opportunity tier. They combine freely with local filters (Web2/Web3, wildcards, target counts, bounty, competition_risks, report counts, free_submission_only, freshness): pages are read in server order and local filters only remove programs; scan.complete and scan.next_cursor report coverage. Sampled sorts need no Activity inputs. ${activityCatalogGuidance}`,
|
|
1394
|
+
inputSchema: z.object({
|
|
1395
|
+
...activityCatalogInput,
|
|
1396
|
+
search: searchTextSchema.optional(),
|
|
1397
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
1398
|
+
page: z.number().int().min(1).optional(),
|
|
1126
1399
|
platforms: platformListSchema(),
|
|
1127
1400
|
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."),
|
|
1128
1401
|
tags: tagListSchema(),
|
|
@@ -1130,7 +1403,11 @@ export function createBbradarServer(client, config) {
|
|
|
1130
1403
|
target_types: targetTypeListSchema(),
|
|
1131
1404
|
language_tags: languageTagListSchema(),
|
|
1132
1405
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
1406
|
+
program_kind: z.enum(["bounty", "contest", "challenge"]).optional().describe("Source-classified program kind; sent to the Pro program list."),
|
|
1133
1407
|
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."),
|
|
1408
|
+
free_submission_only: freeSubmissionOnlySchema,
|
|
1409
|
+
competition_risks: z.array(competitionRiskSchema).max(4).default([]).describe("Program competition labels to keep, e.g. [\"Low\"]."),
|
|
1410
|
+
cursor: z.string().min(1).max(512).optional().describe("scan.next_cursor from the previous result; repeat the same filters."),
|
|
1134
1411
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
1135
1412
|
updated_since: isoDateTimeSchema.optional(),
|
|
1136
1413
|
min_bounty_min: z.number().int().min(0).optional(),
|
|
@@ -1144,7 +1421,7 @@ export function createBbradarServer(client, config) {
|
|
|
1144
1421
|
exclude_mobile_only: z.boolean().default(false),
|
|
1145
1422
|
exclude_non_web: z.boolean().default(false),
|
|
1146
1423
|
has_api_scope: z.boolean().default(false),
|
|
1147
|
-
contest_like: z.boolean().default(false).describe("Require
|
|
1424
|
+
contest_like: z.boolean().default(false).describe("Require source-classified contests."),
|
|
1148
1425
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
1149
1426
|
fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
|
|
1150
1427
|
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
@@ -1154,7 +1431,7 @@ export function createBbradarServer(client, config) {
|
|
|
1154
1431
|
min_added_7d: z.number().int().min(0).optional(),
|
|
1155
1432
|
max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1156
1433
|
max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
|
|
1157
|
-
sort_by: programSortSchema,
|
|
1434
|
+
sort_by: z.union([programSortSchema.removeDefault(), activityCatalogSortSchema]).optional(),
|
|
1158
1435
|
include_target_samples: z.boolean().default(false),
|
|
1159
1436
|
target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
1160
1437
|
target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
|
|
@@ -1162,13 +1439,40 @@ export function createBbradarServer(client, config) {
|
|
|
1162
1439
|
only_bounty_eligible_targets: z.boolean().default(true),
|
|
1163
1440
|
output_mode: programListModeSchema.default("compact"),
|
|
1164
1441
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
1165
|
-
},
|
|
1442
|
+
}).strict(),
|
|
1166
1443
|
outputSchema: findProgramsOutputShape(),
|
|
1167
1444
|
annotations: readOnlyAnnotations
|
|
1168
|
-
}, (args) => runTool("find_programs", config, rateLimiter, async () =>
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1445
|
+
}, (args) => runTool("find_programs", config, rateLimiter, async () => {
|
|
1446
|
+
const serverCatalog = hasActivityCatalogInput(args) || args.snapshot !== undefined || args.page !== undefined || args.cursor !== undefined
|
|
1447
|
+
|| (args.search !== undefined && supportsCatalogFilters(client, args.opportunity_levels.length ? "opportunities" : "programs")
|
|
1448
|
+
&& !legacyCatalogFilters(args).length && ["include", "exclude", "only"].includes(args.vdp_mode));
|
|
1449
|
+
if (serverCatalog && args.cursor === undefined && !legacyCatalogFilters(args).length)
|
|
1450
|
+
return findServerCatalogPrograms(client, config, args);
|
|
1451
|
+
if (serverCatalog) {
|
|
1452
|
+
// Platform, tag, kind, VDP, update and status predicates were applied
|
|
1453
|
+
// by the server; only the remaining filters run locally.
|
|
1454
|
+
return findFilteredServerPrograms(client, config, args, {
|
|
1455
|
+
...args, sort_by: "best_match", platforms: [], tags: [], program_kind: undefined, contest_like: false,
|
|
1456
|
+
vdp_mode: "include", opportunity_levels: [], updated_since: undefined, skip_status_check: true,
|
|
1457
|
+
min_bounty_min: args.min_bounty_min, min_bounty_max: args.min_bounty_max, max_bounty_max: args.max_bounty_max,
|
|
1458
|
+
max_public_report_count: args.max_public_report_count, fresh_launch_days: args.fresh_launch_days,
|
|
1459
|
+
min_eligible_targets: args.min_eligible_targets, min_total_targets: args.min_total_targets,
|
|
1460
|
+
min_added_24h: args.min_added_24h, min_added_7d: args.min_added_7d,
|
|
1461
|
+
target_samples_only_wildcards: args.min_wildcards > 0
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return findPrograms(client, config, {
|
|
1465
|
+
...args,
|
|
1466
|
+
updated_since: args.updated_since,
|
|
1467
|
+
min_bounty_min: args.min_bounty_min, min_bounty_max: args.min_bounty_max,
|
|
1468
|
+
max_bounty_max: args.max_bounty_max, max_public_report_count: args.max_public_report_count,
|
|
1469
|
+
fresh_launch_days: args.fresh_launch_days,
|
|
1470
|
+
min_eligible_targets: args.min_eligible_targets, min_total_targets: args.min_total_targets,
|
|
1471
|
+
min_added_24h: args.min_added_24h, min_added_7d: args.min_added_7d,
|
|
1472
|
+
sort_by: programSortSchema.parse(args.sort_by),
|
|
1473
|
+
target_samples_only_wildcards: args.min_wildcards > 0
|
|
1474
|
+
});
|
|
1475
|
+
}));
|
|
1172
1476
|
registerTool("find_wildcard_programs", {
|
|
1173
1477
|
title: "Find Wildcard Programs",
|
|
1174
1478
|
description: "Wildcard program shortlist with platform, minimum reward, freshness, and report-count filters.",
|
|
@@ -1772,7 +2076,8 @@ export function createBbradarServer(client, config) {
|
|
|
1772
2076
|
...apiEnvelopeOutputShape(),
|
|
1773
2077
|
program: programOutputSchema().optional(),
|
|
1774
2078
|
activity: jsonRecordSchema().optional(),
|
|
1775
|
-
changes: z.array(jsonRecordSchema()).optional()
|
|
2079
|
+
changes: z.array(jsonRecordSchema()).optional(),
|
|
2080
|
+
meta: jsonRecordSchema().optional()
|
|
1776
2081
|
},
|
|
1777
2082
|
annotations: readOnlyAnnotations
|
|
1778
2083
|
}, (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 +2281,15 @@ export function createBbradarServer(client, config) {
|
|
|
1976
2281
|
const exportId = randomUUID();
|
|
1977
2282
|
const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
|
|
1978
2283
|
const resourceUri = exportResourceUri(exportId);
|
|
1979
|
-
|
|
2284
|
+
if (!api.noStore)
|
|
2285
|
+
rememberExport(exportStore, exportId, sanitizedExport);
|
|
1980
2286
|
return withApiMetadata(api, {
|
|
1981
2287
|
source_requests: resolvedProgramIds.sourceRequests.length > 0 ? resolvedProgramIds.sourceRequests : undefined,
|
|
1982
2288
|
warnings: resolvedProgramIds.warnings.length > 0 ? resolvedProgramIds.warnings : undefined,
|
|
1983
2289
|
export: {
|
|
1984
|
-
export_id: exportId,
|
|
1985
|
-
resource_uri: resourceUri,
|
|
2290
|
+
export_id: api.noStore ? undefined : exportId,
|
|
2291
|
+
resource_uri: api.noStore ? undefined : resourceUri,
|
|
2292
|
+
data: api.noStore ? sanitizedExport : undefined,
|
|
1986
2293
|
preview: previewExportPayload(sanitizedExport),
|
|
1987
2294
|
returned_preview_count: previewExportCount(sanitizedExport),
|
|
1988
2295
|
limit: args.limit
|
|
@@ -2079,6 +2386,12 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2079
2386
|
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
2080
2387
|
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
2081
2388
|
},
|
|
2389
|
+
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Choose an enabled period, sort and direction, then call get_activity_rankings with that tab for one server-ranked page. Do not approximate by paging or re-ranking the catalog." },
|
|
2390
|
+
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" }, then: "Use find_programs with the same Activity inputs and competition_risks for program-only results; continue with scan.next_cursor." },
|
|
2391
|
+
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2392
|
+
{ 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." },
|
|
2393
|
+
{ 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." },
|
|
2394
|
+
{ user_goal: "exact recorded activity for one program", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2082
2395
|
{
|
|
2083
2396
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2084
2397
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2221,6 +2534,12 @@ function coreBbradarGuide(topic, client) {
|
|
|
2221
2534
|
first_tool: "get_repository_changes",
|
|
2222
2535
|
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2223
2536
|
},
|
|
2537
|
+
{ user_goal: "program payouts, recent reports, response times or Activity rankings", first_tool: "get_activity_capabilities", arguments: {}, then: "Read supported view combinations and call get_activity_rankings with that tab. Use get_activity_change_detail lazily for a selected Activity event." },
|
|
2538
|
+
{ user_goal: "programs with recent payouts and new scope that also match Web2, competition or fee criteria", first_tool: "find_hunt_targets", arguments: { payout_period: "30d", payout_positive: true, scope_period: "30d", scope_added_min: 1, surface_mode: "web2", free_submission_only: true, ranking_mode: "lowest_competition" } },
|
|
2539
|
+
{ user_goal: "payout and report facts for one exact program", first_tool: "get_program_payouts", arguments: { program_id: "Platform:handle" } },
|
|
2540
|
+
{ user_goal: "semantic program change history", first_tool: "get_program_history", arguments: { program_id: "Platform:handle" }, then: "Fetch change detail only when needed." },
|
|
2541
|
+
{ user_goal: "most active programs", first_tool: "get_analytics_program_activity", arguments: {} },
|
|
2542
|
+
{ user_goal: "exact recorded activity", first_tool: "get_program_activity", arguments: { program_id: "Platform:handle" } },
|
|
2224
2543
|
{
|
|
2225
2544
|
user_goal: "hunt targets combining repository activity with rewards, target quality, platforms, or Web2/Web3 constraints",
|
|
2226
2545
|
first_tool: "find_repository_hunt_targets",
|
|
@@ -2598,7 +2917,7 @@ function getCachedProgramFallback(config, requestedProgramId) {
|
|
|
2598
2917
|
return cloneProgramFallbackForRequest(cached.fallback, requestedProgramId, true);
|
|
2599
2918
|
}
|
|
2600
2919
|
function rememberProgramFallback(config, fallback) {
|
|
2601
|
-
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0) {
|
|
2920
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(fallback.sourceRequests)) {
|
|
2602
2921
|
return;
|
|
2603
2922
|
}
|
|
2604
2923
|
pruneProgramFallbackCache();
|
|
@@ -2623,7 +2942,13 @@ function pruneProgramFallbackCache() {
|
|
|
2623
2942
|
}
|
|
2624
2943
|
}
|
|
2625
2944
|
function programFallbackCacheKey(config, requestedProgramId) {
|
|
2626
|
-
return `${config.apiBaseUrl}\n${normalizeTag(requestedProgramId)}`;
|
|
2945
|
+
return `${config.apiBaseUrl}\n${credentialCacheScope(config)}\n${isStrictProgramId(requestedProgramId) ? requestedProgramId : normalizeTag(requestedProgramId)}`;
|
|
2946
|
+
}
|
|
2947
|
+
function credentialCacheScope(config) {
|
|
2948
|
+
return createHash("sha256").update(config.apiKey).digest("hex");
|
|
2949
|
+
}
|
|
2950
|
+
function hasNoStoreSource(sourceRequests) {
|
|
2951
|
+
return sourceRequests.some((request) => readBoolean(request.no_store) === true);
|
|
2627
2952
|
}
|
|
2628
2953
|
function cloneProgramFallbackForRequest(fallback, requestedProgramId, fromCache) {
|
|
2629
2954
|
const warnings = fromCache
|
|
@@ -3077,7 +3402,8 @@ function getCachedResolveProgram(config, input) {
|
|
|
3077
3402
|
});
|
|
3078
3403
|
}
|
|
3079
3404
|
function rememberResolveProgram(config, input, payload) {
|
|
3080
|
-
|
|
3405
|
+
const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
|
|
3406
|
+
if (config.cacheTtlMs <= 0 || config.cacheMaxEntries <= 0 || hasNoStoreSource(sourceRequests)) {
|
|
3081
3407
|
return;
|
|
3082
3408
|
}
|
|
3083
3409
|
pruneResolveProgramCache();
|
|
@@ -3103,7 +3429,8 @@ function pruneResolveProgramCache() {
|
|
|
3103
3429
|
function resolveProgramCacheKey(config, input) {
|
|
3104
3430
|
return stableStringify({
|
|
3105
3431
|
apiBaseUrl: config.apiBaseUrl,
|
|
3106
|
-
|
|
3432
|
+
credentialScope: credentialCacheScope(config),
|
|
3433
|
+
query: isStrictProgramId(input.query) ? input.query : normalizeTag(input.query),
|
|
3107
3434
|
platforms: input.platforms.map(normalizeTag).sort(),
|
|
3108
3435
|
tags: input.tags.map(normalizeTag).sort(),
|
|
3109
3436
|
opportunity_levels: [...input.opportunity_levels].sort(),
|
|
@@ -4311,8 +4638,9 @@ async function comparePrograms(client, config, input) {
|
|
|
4311
4638
|
});
|
|
4312
4639
|
}
|
|
4313
4640
|
async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
|
|
4314
|
-
const [programApi, changes] = await Promise.all([
|
|
4641
|
+
const [programApi, activityApi, changes] = await Promise.all([
|
|
4315
4642
|
client.getProgram(programId),
|
|
4643
|
+
client.supportsEndpoint("program_activity") ? client.getProgramActivity(programId) : Promise.resolve(undefined),
|
|
4316
4644
|
recentChangesLimit > 0
|
|
4317
4645
|
? fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true)
|
|
4318
4646
|
: Promise.resolve(undefined)
|
|
@@ -4329,15 +4657,18 @@ async function summarizeProgramActivity(client, config, programId, recentChanges
|
|
|
4329
4657
|
upstream_request_id: programApi.upstreamRequestId,
|
|
4330
4658
|
...apiSourceMetadata(programApi)
|
|
4331
4659
|
},
|
|
4660
|
+
...(activityApi ? [{ source: "program_activity", request_id: activityApi.requestId, upstream_request_id: activityApi.upstreamRequestId, ...apiSourceMetadata(activityApi) }] : []),
|
|
4332
4661
|
...(changes?.sourceRequests ?? [])
|
|
4333
4662
|
],
|
|
4334
4663
|
program,
|
|
4335
4664
|
activity: stripUndefined({
|
|
4665
|
+
activity_summary: activityApi ? sanitizeProPayload(readObject(activityApi.data)?.activity_summary) : undefined,
|
|
4336
4666
|
target_counts: readObject(scopeSummary?.target_counts),
|
|
4337
4667
|
target_activity: targetActivity,
|
|
4338
4668
|
recent_change_count: changes?.changes.length
|
|
4339
4669
|
}),
|
|
4340
|
-
changes: changes?.changes ?? []
|
|
4670
|
+
changes: changes?.changes ?? [],
|
|
4671
|
+
meta: { target_change_samples_are_semantic_history: false, activity_source: activityApi ? "pro_activity_endpoint" : "legacy_program_projection" }
|
|
4341
4672
|
};
|
|
4342
4673
|
}
|
|
4343
4674
|
async function getLatestAddedTargets(client, config, input) {
|
|
@@ -4593,10 +4924,12 @@ async function fetchProgramTargetsWithEndpointFallback(client, programId) {
|
|
|
4593
4924
|
requestId: api.requestId,
|
|
4594
4925
|
upstreamRequestId: api.upstreamRequestId,
|
|
4595
4926
|
fetchedAt: api.fetchedAt,
|
|
4927
|
+
noStore: api.noStore,
|
|
4596
4928
|
cache: readObject(stripUndefined({
|
|
4597
4929
|
hit: api.cached,
|
|
4598
4930
|
coalesced_live_request: api.coalesced,
|
|
4599
|
-
expires_at: api.cacheExpiresAt
|
|
4931
|
+
expires_at: api.cacheExpiresAt,
|
|
4932
|
+
no_store: api.noStore
|
|
4600
4933
|
})),
|
|
4601
4934
|
source: "program_targets",
|
|
4602
4935
|
rawTargets,
|
|
@@ -4654,7 +4987,7 @@ function getCachedProgramTargetsFallback(client, programId) {
|
|
|
4654
4987
|
}
|
|
4655
4988
|
function rememberProgramTargetsFallback(client, programId, lookup) {
|
|
4656
4989
|
const diagnostics = client.diagnostics();
|
|
4657
|
-
if (!diagnostics.response_cache.enabled || lookup.unavailable) {
|
|
4990
|
+
if (!diagnostics.response_cache.enabled || lookup.unavailable || lookup.noStore) {
|
|
4658
4991
|
return;
|
|
4659
4992
|
}
|
|
4660
4993
|
const cache = programTargetsFallbackCacheForClient(client);
|
|
@@ -4692,7 +5025,7 @@ function pruneProgramTargetsFallbackCache(cache) {
|
|
|
4692
5025
|
}
|
|
4693
5026
|
}
|
|
4694
5027
|
function programTargetsFallbackCacheKey(programId) {
|
|
4695
|
-
return
|
|
5028
|
+
return programId;
|
|
4696
5029
|
}
|
|
4697
5030
|
function cacheTtlMsFromDiagnostics(cache) {
|
|
4698
5031
|
return cache.enabled ? cache.ttl_ms : 0;
|
|
@@ -4711,10 +5044,12 @@ async function fetchProgramTargetsFromExportFallback(client, programId) {
|
|
|
4711
5044
|
requestId: api.requestId,
|
|
4712
5045
|
upstreamRequestId: api.upstreamRequestId,
|
|
4713
5046
|
fetchedAt: api.fetchedAt,
|
|
5047
|
+
noStore: api.noStore,
|
|
4714
5048
|
cache: readObject(stripUndefined({
|
|
4715
5049
|
hit: api.cached,
|
|
4716
5050
|
coalesced_live_request: api.coalesced,
|
|
4717
|
-
expires_at: api.cacheExpiresAt
|
|
5051
|
+
expires_at: api.cacheExpiresAt,
|
|
5052
|
+
no_store: api.noStore
|
|
4718
5053
|
})),
|
|
4719
5054
|
source: "target_export",
|
|
4720
5055
|
rawTargets,
|
|
@@ -4761,7 +5096,8 @@ async function getProgramFromTargetsOnlyFallback(client, programId, programError
|
|
|
4761
5096
|
cache: readObject(stripUndefined({
|
|
4762
5097
|
hit: targetsApi.cached,
|
|
4763
5098
|
coalesced_live_request: targetsApi.coalesced,
|
|
4764
|
-
expires_at: targetsApi.cacheExpiresAt
|
|
5099
|
+
expires_at: targetsApi.cacheExpiresAt,
|
|
5100
|
+
no_store: targetsApi.noStore
|
|
4765
5101
|
})),
|
|
4766
5102
|
source_requests: [
|
|
4767
5103
|
stripUndefined({
|
|
@@ -4906,8 +5242,6 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4906
5242
|
generated_at: new Date().toISOString()
|
|
4907
5243
|
});
|
|
4908
5244
|
const exportId = randomUUID();
|
|
4909
|
-
const resourceUri = exportResourceUri(exportId);
|
|
4910
|
-
rememberExport(exportStore, exportId, exportPayload);
|
|
4911
5245
|
return stripUndefined({
|
|
4912
5246
|
request_id: randomUUID(),
|
|
4913
5247
|
source_requests: sourceRequests,
|
|
@@ -4915,7 +5249,7 @@ async function exportTargetsFromProgramTargetsFallback(client, config, exportSto
|
|
|
4915
5249
|
errors: errors.length > 0 ? errors : undefined,
|
|
4916
5250
|
export: {
|
|
4917
5251
|
export_id: exportId,
|
|
4918
|
-
|
|
5252
|
+
data: exportPayload,
|
|
4919
5253
|
preview: previewExportPayload(exportPayload),
|
|
4920
5254
|
returned_preview_count: previewExportCount(exportPayload),
|
|
4921
5255
|
limit: input.limit
|
|
@@ -5642,6 +5976,10 @@ function applyCollectionContinuation(payload, field, label, store, config) {
|
|
|
5642
5976
|
if (!items || items.length <= inlineLimit) {
|
|
5643
5977
|
return payload;
|
|
5644
5978
|
}
|
|
5979
|
+
// A no-store page stays only in this tool result, never in a reusable resource.
|
|
5980
|
+
if (readBoolean(readObject(payload.cache)?.no_store) === true) {
|
|
5981
|
+
return payload;
|
|
5982
|
+
}
|
|
5645
5983
|
const continuationId = randomUUID();
|
|
5646
5984
|
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5647
5985
|
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
@@ -5898,10 +6236,12 @@ function formatProgram(program, mode) {
|
|
|
5898
6236
|
function compactProgram(program) {
|
|
5899
6237
|
return stripUndefined({
|
|
5900
6238
|
id: stringField(program, "id"),
|
|
6239
|
+
catalog_id: stringField(program, "catalog_id"),
|
|
5901
6240
|
handle: stringField(program, "handle"),
|
|
5902
6241
|
name: stringField(program, "name"),
|
|
5903
6242
|
platform: stringField(program, "platform"),
|
|
5904
6243
|
catalog: stringField(program, "catalog"),
|
|
6244
|
+
program_kind: stringField(program, "program_kind"),
|
|
5905
6245
|
program_status: stringField(program, "program_status"),
|
|
5906
6246
|
is_active: readBoolean(program.is_active),
|
|
5907
6247
|
program_type: stringField(program, "program_type"),
|
|
@@ -5910,15 +6250,24 @@ function compactProgram(program) {
|
|
|
5910
6250
|
policy_url: stringField(program, "policy_url"),
|
|
5911
6251
|
reporting_url: stringField(program, "reporting_url"),
|
|
5912
6252
|
reward_status: stringField(program, "reward_status"),
|
|
5913
|
-
data_capabilities:
|
|
5914
|
-
opportunity_tag:
|
|
6253
|
+
data_capabilities: program.data_capabilities,
|
|
6254
|
+
opportunity_tag: program.opportunity_tag,
|
|
5915
6255
|
competition_risk: stringField(program, "competition_risk"),
|
|
5916
6256
|
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5917
|
-
submission_cost:
|
|
5918
|
-
|
|
5919
|
-
|
|
6257
|
+
submission_cost: program.submission_cost,
|
|
6258
|
+
payout_summary: program.payout_summary,
|
|
6259
|
+
activity: program.activity,
|
|
6260
|
+
activity_summary: program.activity_summary,
|
|
6261
|
+
reward_range: program.reward_range,
|
|
6262
|
+
reward_range_label: stringField(program, "reward_range_label"),
|
|
6263
|
+
public_report_count: program.public_report_count,
|
|
5920
6264
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
5921
6265
|
first_seen_date: stringField(program, "first_seen_date"),
|
|
6266
|
+
launch_at: program.launch_at,
|
|
6267
|
+
launch_at_precision: stringField(program, "launch_at_precision"),
|
|
6268
|
+
ends_at: program.ends_at,
|
|
6269
|
+
ends_on: program.ends_on,
|
|
6270
|
+
verification: program.verification,
|
|
5922
6271
|
last_updated: stringField(program, "last_updated"),
|
|
5923
6272
|
targets_updated_at: stringField(program, "targets_updated_at"),
|
|
5924
6273
|
scope_summary: readObject(program.scope_summary),
|
|
@@ -6240,7 +6589,7 @@ function candidateLooksLikeCtf(candidate) {
|
|
|
6240
6589
|
return candidate.looksLikeCtf;
|
|
6241
6590
|
}
|
|
6242
6591
|
function candidateLooksContestLike(candidate) {
|
|
6243
|
-
return
|
|
6592
|
+
return stringField(candidate.program, "program_kind") === "contest";
|
|
6244
6593
|
}
|
|
6245
6594
|
function candidateHasKnownNoBounty(candidate) {
|
|
6246
6595
|
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
@@ -6467,7 +6816,10 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6467
6816
|
warnings.push(`${name} was clamped from ${value} to ${max}.`);
|
|
6468
6817
|
return max;
|
|
6469
6818
|
}
|
|
6470
|
-
|
|
6819
|
+
// Candidate pool for hunts narrowed by Activity on the server. Matches arrive in
|
|
6820
|
+
// server order, so a larger pool lets ranking_mode choose among more of them.
|
|
6821
|
+
const HUNT_SERVER_POOL = 100;
|
|
6822
|
+
async function findHuntTargets(client, config, input, activity = {}) {
|
|
6471
6823
|
const warnings = [];
|
|
6472
6824
|
const errors = [];
|
|
6473
6825
|
const selfHostedOnly = isSelfHostedOnlyPlatformSelection(input.platforms);
|
|
@@ -6510,6 +6862,7 @@ async function findHuntTargets(client, config, input) {
|
|
|
6510
6862
|
min_total_targets: input.min_total_targets,
|
|
6511
6863
|
min_added_24h: undefined,
|
|
6512
6864
|
min_added_7d: input.min_added_7d,
|
|
6865
|
+
free_submission_only: input.free_submission_only,
|
|
6513
6866
|
max_pages: input.max_program_pages,
|
|
6514
6867
|
max_results: candidatePoolSize,
|
|
6515
6868
|
sort_by: huntProgramSort(input.ranking_mode),
|
|
@@ -6531,8 +6884,17 @@ async function findHuntTargets(client, config, input) {
|
|
|
6531
6884
|
else if (recentChangeQueries.length < desiredRecentChangeQueries.length) {
|
|
6532
6885
|
warnings.push(`Recent target-change queries were reduced from ${desiredRecentChangeQueries.length} to ${recentChangeQueries.length} to preserve the upstream request budget.`);
|
|
6533
6886
|
}
|
|
6887
|
+
const serverFiltered = hasActivityCatalogInput(activity);
|
|
6534
6888
|
const [programPayload, recentResult] = await Promise.all([
|
|
6535
|
-
|
|
6889
|
+
serverFiltered
|
|
6890
|
+
? findFilteredServerPrograms(client, config, {
|
|
6891
|
+
...programInput, ...activity, opportunity_levels: [], sort_by: activity.sort_by, sort_direction: activity.sort_direction,
|
|
6892
|
+
platforms: input.platforms, tags: input.tags, updated_since: input.updated_since, vdp_mode: "exclude", include_target_samples: false
|
|
6893
|
+
}, {
|
|
6894
|
+
...programInput, platforms: [], tags: [], vdp_mode: "include", updated_since: undefined,
|
|
6895
|
+
max_pages: programRequestBudget, upstream_request_budget: programRequestBudget
|
|
6896
|
+
}, HUNT_SERVER_POOL)
|
|
6897
|
+
: findPrograms(client, config, programInput),
|
|
6536
6898
|
recentChangesEnabled
|
|
6537
6899
|
? fetchHuntRecentChanges(client, config, input, recentChangeQueries)
|
|
6538
6900
|
: Promise.resolve({ changes: [], sourceRequests: [], requestsUsed: 0, errors: [] })
|
|
@@ -6602,6 +6964,8 @@ async function findHuntTargets(client, config, input) {
|
|
|
6602
6964
|
min_target_score: input.min_target_score,
|
|
6603
6965
|
target_opportunity_label: input.target_opportunity_label,
|
|
6604
6966
|
competition_risk: input.competition_risk,
|
|
6967
|
+
free_submission_only: input.free_submission_only || undefined,
|
|
6968
|
+
activity: serverFiltered ? stripUndefined(activity) : undefined,
|
|
6605
6969
|
recent_target_days: input.include_recent_changes ? input.recent_target_days : undefined,
|
|
6606
6970
|
ranking_mode: input.ranking_mode
|
|
6607
6971
|
}),
|
|
@@ -6612,10 +6976,12 @@ async function findHuntTargets(client, config, input) {
|
|
|
6612
6976
|
},
|
|
6613
6977
|
ranking_scope: stripUndefined({
|
|
6614
6978
|
...(readObject(programPayload.ranking_scope) ?? {}),
|
|
6979
|
+
local_ranking: serverFiltered ? input.ranking_mode : undefined,
|
|
6615
6980
|
single_program_catalog_pass: true,
|
|
6616
6981
|
recent_change_window_days: input.include_recent_changes ? input.recent_target_days : undefined,
|
|
6617
6982
|
recent_change_page_size: input.include_recent_changes ? input.recent_changes_page_size : undefined
|
|
6618
6983
|
}),
|
|
6984
|
+
scan: readObject(programPayload.scan),
|
|
6619
6985
|
result_guidance: {
|
|
6620
6986
|
final_shortlist: true,
|
|
6621
6987
|
follow_up_calls_required: false,
|
|
@@ -7328,6 +7694,262 @@ function uniqueProgramsFromRepositoryMatches(matches) {
|
|
|
7328
7694
|
}
|
|
7329
7695
|
return [...programs.values()];
|
|
7330
7696
|
}
|
|
7697
|
+
/** Filters the MCP applies to program records after retrieval. */
|
|
7698
|
+
function localCatalogFilters(input) {
|
|
7699
|
+
const keys = ["exclude_platforms", "scope_tags", "target_types", "language_tags", "web3", "exclude_web3",
|
|
7700
|
+
"min_bounty_min", "min_bounty_max", "max_bounty_max", "require_bounty", "max_public_report_count",
|
|
7701
|
+
"require_known_report_count", "include_unknown_report_count", "exclude_ctf", "exclude_mobile_only",
|
|
7702
|
+
"exclude_non_web", "has_api_scope", "fresh_launch_days", "min_eligible_targets", "min_total_targets",
|
|
7703
|
+
"min_added_24h", "min_added_7d", "free_submission_only", "competition_risks"].filter(key => {
|
|
7704
|
+
const value = input[key];
|
|
7705
|
+
return Array.isArray(value) ? value.length > 0 : value !== undefined && value !== false;
|
|
7706
|
+
});
|
|
7707
|
+
if (Number(input.min_wildcards) > 0)
|
|
7708
|
+
keys.push("min_wildcards");
|
|
7709
|
+
return keys;
|
|
7710
|
+
}
|
|
7711
|
+
function legacyCatalogFilters(input) {
|
|
7712
|
+
return input.include_target_samples === true ? [...localCatalogFilters(input), "include_target_samples"] : localCatalogFilters(input);
|
|
7713
|
+
}
|
|
7714
|
+
function supportsCatalogFilters(client, endpoint) {
|
|
7715
|
+
return client.capabilitySnapshot().status === "negotiated"
|
|
7716
|
+
&& readObject(client.endpointCapability(endpoint)?.catalog_filters)?.enabled === true;
|
|
7717
|
+
}
|
|
7718
|
+
function assertCatalogQuerySupported(client, endpoint, input) {
|
|
7719
|
+
if (!["include", "exclude", "only"].includes(String(input.vdp_mode ?? "exclude"))) {
|
|
7720
|
+
throw new Error("Server-ranked queries accept vdp_mode include, exclude or only. The heuristic only_likely and only_known_no_bounty modes are available only in legacy sampled discovery.");
|
|
7721
|
+
}
|
|
7722
|
+
const capability = client.endpointCapability(endpoint);
|
|
7723
|
+
if (!supportsCatalogFilters(client, endpoint)) {
|
|
7724
|
+
throw new Error("This API deployment does not advertise the required catalog filters. Upgrade the API, or use legacy discovery without server paging or Activity filters.");
|
|
7725
|
+
}
|
|
7726
|
+
if (requiresActivityQuery(input) && readObject(capability?.activity_filters)?.enabled !== true) {
|
|
7727
|
+
throw new Error("Activity catalog filters are unavailable on this API deployment. Ordinary program discovery remains available.");
|
|
7728
|
+
}
|
|
7729
|
+
const kind = input.program_kind ?? (input.contest_like ? "contest" : undefined);
|
|
7730
|
+
if (kind && !readArray(capability?.program_kind_filter).includes(kind)) {
|
|
7731
|
+
throw new Error(`This API deployment does not support program_kind=${String(kind)} on ${endpoint}; the filter was not sent or discarded.`);
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
function hasActivityCatalogInput(input) {
|
|
7735
|
+
return Object.keys(activityCatalogInput).some((key) => key === "sort_by"
|
|
7736
|
+
? activityCatalogSortSchema.safeParse(input[key]).success
|
|
7737
|
+
: input[key] !== undefined);
|
|
7738
|
+
}
|
|
7739
|
+
/** Validates and builds one server-ordered catalog query; nothing is sent. */
|
|
7740
|
+
function serverCatalogQuery(client, input) {
|
|
7741
|
+
const levels = Array.isArray(input.opportunity_levels) ? input.opportunity_levels : [];
|
|
7742
|
+
if (levels.length > 1)
|
|
7743
|
+
throw new Error("Combined Activity queries accept at most one opportunity tier; separate tier pages cannot form a global ranking.");
|
|
7744
|
+
if (input.sort_by !== undefined && !activityCatalogSortSchema.safeParse(input.sort_by).success) {
|
|
7745
|
+
throw new Error("Combined Activity queries require an enabled server sort_by. Omit it for updated descending; legacy sampled sorts cannot rank these results.");
|
|
7746
|
+
}
|
|
7747
|
+
if (input.contest_like && input.program_kind && input.program_kind !== "contest")
|
|
7748
|
+
throw new Error("contest_like conflicts with program_kind.");
|
|
7749
|
+
const endpoint = levels.length ? "opportunities" : "programs";
|
|
7750
|
+
assertCatalogQuerySupported(client, endpoint, input);
|
|
7751
|
+
assertStableSnapshotSupported(client, endpoint, typeof input.snapshot === "string" ? input.snapshot : undefined);
|
|
7752
|
+
const signals = Object.fromEntries(Object.keys(activityCatalogInput).filter((key) => input[key] !== undefined).map((key) => [key, input[key]]));
|
|
7753
|
+
return {
|
|
7754
|
+
endpoint, level: levels[0], maxPageSize: client.endpointNumber(endpoint, "max_page_size", MAX_PROGRAMS_PER_SEARCH),
|
|
7755
|
+
query: { ...signals, search: input.search, platforms: input.platforms, tags: input.tags,
|
|
7756
|
+
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined), updated_since: input.updated_since,
|
|
7757
|
+
vdp_mode: input.vdp_mode, snapshot: input.snapshot }
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
function readServerCatalogPage(client, catalog, page, pageSize, snapshot) {
|
|
7761
|
+
const query = toQuery({ ...catalog.query, snapshot, page, page_size: pageSize });
|
|
7762
|
+
return catalog.level ? client.getOpportunities(catalog.level, query) : client.listPrograms(query);
|
|
7763
|
+
}
|
|
7764
|
+
async function findServerCatalogPrograms(client, config, input) {
|
|
7765
|
+
// One exact server page: page and snapshot continue it. Local filters use the
|
|
7766
|
+
// ordered scan below instead.
|
|
7767
|
+
const catalog = serverCatalogQuery(client, input);
|
|
7768
|
+
const pageSize = Number(input.max_results) || 10;
|
|
7769
|
+
if (pageSize > catalog.maxPageSize)
|
|
7770
|
+
throw new Error(`Combined Activity pages support max_results up to ${catalog.maxPageSize}; use page and the returned stable snapshot to continue.`);
|
|
7771
|
+
const api = await readServerCatalogPage(client, catalog, Number(input.page ?? 1), pageSize, input.snapshot);
|
|
7772
|
+
const data = readObject(api.data);
|
|
7773
|
+
const mode = input.output_mode === "full" ? "full" : "compact";
|
|
7774
|
+
return withApiMetadata(api, {
|
|
7775
|
+
programs: readArray(data?.programs).map((program) => formatProgram(sanitizeProgram(program, config.webBaseUrl), mode)),
|
|
7776
|
+
ranking_scope: { mode: "server", sort_by: input.sort_by ?? "updated", sort_direction: input.sort_direction ?? "desc" },
|
|
7777
|
+
meta: sanitizeJson(data?.meta)
|
|
7778
|
+
});
|
|
7779
|
+
}
|
|
7780
|
+
// Pages requested together while scanning. Activity catalog reads share the
|
|
7781
|
+
// two-request changes pool, so a wider window would only queue.
|
|
7782
|
+
const CATALOG_SCAN_PREFETCH = 2;
|
|
7783
|
+
function encodeScanCursor(cursor) {
|
|
7784
|
+
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
7785
|
+
}
|
|
7786
|
+
function decodeScanCursor(value, fingerprint, pageSize) {
|
|
7787
|
+
let cursor;
|
|
7788
|
+
try {
|
|
7789
|
+
cursor = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
7790
|
+
}
|
|
7791
|
+
catch {
|
|
7792
|
+
cursor = undefined;
|
|
7793
|
+
}
|
|
7794
|
+
if (!cursor || cursor.v !== 1 || !Number.isInteger(cursor.p) || cursor.p < 1 || !Number.isInteger(cursor.k) || cursor.k < 0) {
|
|
7795
|
+
throw new Error("Invalid scan cursor. Use scan.next_cursor from the previous find_programs result.");
|
|
7796
|
+
}
|
|
7797
|
+
if (cursor.f !== fingerprint || cursor.s !== pageSize) {
|
|
7798
|
+
throw new Error("This scan cursor belongs to different filters. Repeat the previous filters exactly, or start again without cursor.");
|
|
7799
|
+
}
|
|
7800
|
+
return cursor;
|
|
7801
|
+
}
|
|
7802
|
+
/**
|
|
7803
|
+
* Reads server-ordered pages and keeps the programs that also pass the local
|
|
7804
|
+
* filters, until enough match or the page/request budget is spent. Filtering
|
|
7805
|
+
* never reorders, so the result is a prefix of the server's global order.
|
|
7806
|
+
*/
|
|
7807
|
+
async function scanServerCatalog(client, config, input, local, limits) {
|
|
7808
|
+
const catalog = serverCatalogQuery(client, input);
|
|
7809
|
+
const pageSize = catalog.maxPageSize;
|
|
7810
|
+
const localFilters = localCatalogFilters(input);
|
|
7811
|
+
const fingerprint = createHash("sha256")
|
|
7812
|
+
.update(JSON.stringify([catalog.endpoint, catalog.level, toQuery({ ...catalog.query, snapshot: undefined }), localFilters.map((key) => [key, input[key]])]))
|
|
7813
|
+
.digest("base64url").slice(0, 16);
|
|
7814
|
+
const cursorValue = typeof input.cursor === "string" ? input.cursor : undefined;
|
|
7815
|
+
if (cursorValue && input.page !== undefined)
|
|
7816
|
+
throw new Error("Use either cursor or page, not both.");
|
|
7817
|
+
const start = cursorValue ? decodeScanCursor(cursorValue, fingerprint, pageSize)
|
|
7818
|
+
: { v: 1, p: Number(input.page ?? 1), k: 0, s: pageSize, f: fingerprint, a: undefined, t: undefined };
|
|
7819
|
+
const sourceRequests = [];
|
|
7820
|
+
let matches = [];
|
|
7821
|
+
let seen = new Set();
|
|
7822
|
+
let nextPage = start.p;
|
|
7823
|
+
let skip = start.k;
|
|
7824
|
+
let version = start.a;
|
|
7825
|
+
let token = start.t ?? input.snapshot;
|
|
7826
|
+
let totalPages;
|
|
7827
|
+
let serverTotal;
|
|
7828
|
+
let rowsScanned = 0;
|
|
7829
|
+
let pagesRead = 0;
|
|
7830
|
+
let restarted = false;
|
|
7831
|
+
let exhausted = false;
|
|
7832
|
+
let resume;
|
|
7833
|
+
scan: while (matches.length < limits.maxResults) {
|
|
7834
|
+
const pagesLeft = totalPages === undefined ? Number.POSITIVE_INFINITY : totalPages - nextPage + 1;
|
|
7835
|
+
const allowance = Math.min(limits.maxPages - pagesRead, limits.budget.remaining, pagesLeft);
|
|
7836
|
+
if (allowance <= 0)
|
|
7837
|
+
break;
|
|
7838
|
+
// Speculate only from an observed match rate, never before the first page.
|
|
7839
|
+
const rate = rowsScanned > 0 ? Math.max(matches.length / rowsScanned, 0.05) : 1;
|
|
7840
|
+
const needed = pagesRead === 0 ? 1 : Math.ceil((limits.maxResults - matches.length) / (rate * pageSize));
|
|
7841
|
+
const batch = Math.max(1, Math.min(allowance, needed, CATALOG_SCAN_PREFETCH));
|
|
7842
|
+
const pageToken = token;
|
|
7843
|
+
const pages = await Promise.all(Array.from({ length: batch }, (_, index) => nextPage + index)
|
|
7844
|
+
.map(async (page) => ({ page, api: await readServerCatalogPage(client, catalog, page, pageSize, pageToken) })));
|
|
7845
|
+
for (const { page, api } of pages) {
|
|
7846
|
+
pagesRead += 1;
|
|
7847
|
+
limits.budget.remaining -= api.cached || api.coalesced ? 0 : 1;
|
|
7848
|
+
sourceRequests.push({ source: catalog.level ? `opportunities:${catalog.level}` : "programs", page, request_id: api.requestId,
|
|
7849
|
+
upstream_request_id: api.upstreamRequestId, ...apiSourceMetadata(api) });
|
|
7850
|
+
const data = readObject(api.data);
|
|
7851
|
+
const meta = readObject(data?.meta);
|
|
7852
|
+
const pageVersion = stringField(meta ?? {}, "activity_snapshot_id");
|
|
7853
|
+
if (version && pageVersion && pageVersion !== version) {
|
|
7854
|
+
// Never mix rows from two Activity versions. Within one call, restart
|
|
7855
|
+
// once on the new version; a continued scan must be restarted by the caller.
|
|
7856
|
+
if (cursorValue || restarted) {
|
|
7857
|
+
throw new Error("The Activity version changed while scanning. Rerun the same query without cursor.");
|
|
7858
|
+
}
|
|
7859
|
+
restarted = true;
|
|
7860
|
+
matches = [];
|
|
7861
|
+
seen = new Set();
|
|
7862
|
+
rowsScanned = 0;
|
|
7863
|
+
nextPage = start.p;
|
|
7864
|
+
skip = start.k;
|
|
7865
|
+
totalPages = undefined;
|
|
7866
|
+
version = pageVersion;
|
|
7867
|
+
continue scan;
|
|
7868
|
+
}
|
|
7869
|
+
version ??= pageVersion;
|
|
7870
|
+
totalPages = readNumber(meta?.total_pages) ?? totalPages;
|
|
7871
|
+
serverTotal = readNumber(meta?.total) ?? serverTotal;
|
|
7872
|
+
if (token !== undefined && typeof meta?.snapshot === "string")
|
|
7873
|
+
token = meta.snapshot;
|
|
7874
|
+
const rows = readArray(data?.programs);
|
|
7875
|
+
const lastPage = rows.length < pageSize || (totalPages !== undefined && page >= totalPages);
|
|
7876
|
+
for (let index = skip; index < rows.length; index += 1) {
|
|
7877
|
+
rowsScanned += 1;
|
|
7878
|
+
const candidate = toProgramCandidate(rows[index], config.webBaseUrl);
|
|
7879
|
+
const id = stringField(candidate.program, "id") ?? `${page}:${index}`;
|
|
7880
|
+
if (seen.has(id) || !programCandidateMatches(candidate, local))
|
|
7881
|
+
continue;
|
|
7882
|
+
seen.add(id);
|
|
7883
|
+
matches.push(candidate);
|
|
7884
|
+
if (matches.length >= limits.maxResults) {
|
|
7885
|
+
resume = index + 1 < rows.length ? { p: page, k: index + 1 } : lastPage ? undefined : { p: page + 1, k: 0 };
|
|
7886
|
+
exhausted = resume === undefined;
|
|
7887
|
+
break scan;
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
skip = 0;
|
|
7891
|
+
if (lastPage) {
|
|
7892
|
+
exhausted = true;
|
|
7893
|
+
break scan;
|
|
7894
|
+
}
|
|
7895
|
+
nextPage = page + 1;
|
|
7896
|
+
}
|
|
7897
|
+
}
|
|
7898
|
+
if (!exhausted && !resume)
|
|
7899
|
+
resume = { p: nextPage, k: skip };
|
|
7900
|
+
return {
|
|
7901
|
+
candidates: matches,
|
|
7902
|
+
sourceRequests,
|
|
7903
|
+
scan: stripUndefined({
|
|
7904
|
+
order: "Server order; local filters only remove programs.",
|
|
7905
|
+
sort_by: input.sort_by ?? "updated",
|
|
7906
|
+
sort_direction: input.sort_direction ?? "desc",
|
|
7907
|
+
local_filters: localFilters.length > 0 ? localFilters : undefined,
|
|
7908
|
+
server_matches: serverTotal,
|
|
7909
|
+
rows_scanned: rowsScanned,
|
|
7910
|
+
pages_read: pagesRead,
|
|
7911
|
+
complete: exhausted,
|
|
7912
|
+
next_cursor: resume
|
|
7913
|
+
? encodeScanCursor({ v: 1, p: resume.p, k: resume.k, s: pageSize, f: fingerprint, a: version, t: typeof token === "string" ? token : undefined })
|
|
7914
|
+
: undefined,
|
|
7915
|
+
activity_snapshot_id: version,
|
|
7916
|
+
restarted_on_new_activity_version: restarted || undefined
|
|
7917
|
+
})
|
|
7918
|
+
};
|
|
7919
|
+
}
|
|
7920
|
+
/** find_programs with server Activity filters plus local filters or target samples. */
|
|
7921
|
+
async function findFilteredServerPrograms(client, config, input, local, maxResults = local.max_results) {
|
|
7922
|
+
const normalized = normalizeFindProgramsInput(local);
|
|
7923
|
+
const warnings = normalized.warnings;
|
|
7924
|
+
local = normalized.input;
|
|
7925
|
+
const budget = { initial: local.upstream_request_budget, remaining: local.upstream_request_budget };
|
|
7926
|
+
const result = await scanServerCatalog(client, config, input, local, { maxResults, maxPages: local.max_pages, budget });
|
|
7927
|
+
if (result.scan.complete !== true) {
|
|
7928
|
+
warnings.push("More server pages remain; scan.next_cursor continues in the same order.");
|
|
7929
|
+
}
|
|
7930
|
+
const samplePrograms = Math.min(local.target_sample_programs, result.candidates.length, Math.max(0, budget.remaining));
|
|
7931
|
+
const targetSamples = local.include_target_samples && samplePrograms > 0
|
|
7932
|
+
? await collectTargetSamples(client, result.candidates, { ...local, target_sample_programs: samplePrograms }, budget)
|
|
7933
|
+
: {};
|
|
7934
|
+
const outputMode = local.output_mode ?? "compact";
|
|
7935
|
+
return stripUndefined({
|
|
7936
|
+
request_id: randomUUID(),
|
|
7937
|
+
fetched_at: new Date().toISOString(),
|
|
7938
|
+
source_requests: result.sourceRequests,
|
|
7939
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
7940
|
+
upstream_budget: { used: budget.initial - budget.remaining, remaining: budget.remaining },
|
|
7941
|
+
ranking_scope: { mode: "server_filtered", sort_by: result.scan.sort_by, sort_direction: result.scan.sort_direction, complete: result.scan.complete },
|
|
7942
|
+
scan: result.scan,
|
|
7943
|
+
programs: result.candidates.map((candidate, index) => candidateOutput(candidate, index, targetSamples, outputMode)),
|
|
7944
|
+
meta: { returned: result.candidates.length, server_matches: result.scan.server_matches }
|
|
7945
|
+
});
|
|
7946
|
+
}
|
|
7947
|
+
function programHasNoSubmissionFee(program) {
|
|
7948
|
+
// Mirrors the API: no detected cost, or a detected zero with no maximum.
|
|
7949
|
+
const cost = readObject(program.submission_cost);
|
|
7950
|
+
const amount = numberField(cost, "amount");
|
|
7951
|
+
return amount === undefined || (amount === 0 && (numberField(cost, "maximum_amount") ?? 0) === 0);
|
|
7952
|
+
}
|
|
7331
7953
|
async function findPrograms(client, config, input) {
|
|
7332
7954
|
const normalized = normalizeFindProgramsInput(input);
|
|
7333
7955
|
const warnings = normalized.warnings;
|
|
@@ -7344,8 +7966,10 @@ async function findPrograms(client, config, input) {
|
|
|
7344
7966
|
...(input.min_wildcards > 0 ? ["wildcard"] : [])
|
|
7345
7967
|
]);
|
|
7346
7968
|
const collected = await collectPrograms(client, {
|
|
7969
|
+
search: input.search,
|
|
7347
7970
|
platforms: input.platforms,
|
|
7348
7971
|
tags: upstreamTags,
|
|
7972
|
+
program_kind: input.program_kind ?? (input.contest_like ? "contest" : undefined),
|
|
7349
7973
|
updated_since: input.updated_since,
|
|
7350
7974
|
opportunity_levels: input.opportunity_levels,
|
|
7351
7975
|
max_pages: input.max_pages,
|
|
@@ -7394,6 +8018,7 @@ async function findPrograms(client, config, input) {
|
|
|
7394
8018
|
exclude_non_web: input.exclude_non_web,
|
|
7395
8019
|
has_api_scope: input.has_api_scope,
|
|
7396
8020
|
contest_like: input.contest_like ? true : undefined,
|
|
8021
|
+
program_kind: input.program_kind,
|
|
7397
8022
|
vdp_mode: input.vdp_mode && input.vdp_mode !== "include" ? input.vdp_mode : undefined,
|
|
7398
8023
|
fresh_launch_days: input.fresh_launch_days,
|
|
7399
8024
|
min_wildcards: input.min_wildcards,
|
|
@@ -7401,6 +8026,8 @@ async function findPrograms(client, config, input) {
|
|
|
7401
8026
|
min_total_targets: input.min_total_targets,
|
|
7402
8027
|
min_added_24h: input.min_added_24h,
|
|
7403
8028
|
min_added_7d: input.min_added_7d,
|
|
8029
|
+
free_submission_only: input.free_submission_only || undefined,
|
|
8030
|
+
competition_risks: input.competition_risks?.length ? input.competition_risks : undefined,
|
|
7404
8031
|
sort_by: input.sort_by,
|
|
7405
8032
|
output_mode: outputMode
|
|
7406
8033
|
}),
|
|
@@ -7823,6 +8450,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
|
|
|
7823
8450
|
platforms: options.platforms,
|
|
7824
8451
|
tags: options.tags,
|
|
7825
8452
|
updated_since: options.updated_since,
|
|
8453
|
+
program_kind: source.kind === "programs" ? options.program_kind : undefined,
|
|
7826
8454
|
page,
|
|
7827
8455
|
page_size: MAX_PROGRAMS_PER_SEARCH
|
|
7828
8456
|
});
|
|
@@ -7908,8 +8536,11 @@ function toProgramCandidate(rawProgram, webBaseUrl) {
|
|
|
7908
8536
|
return candidate;
|
|
7909
8537
|
}
|
|
7910
8538
|
function programCandidateMatches(candidate, filters) {
|
|
8539
|
+
if (filters.program_kind && stringField(candidate.program, "program_kind") !== filters.program_kind) {
|
|
8540
|
+
return false;
|
|
8541
|
+
}
|
|
7911
8542
|
const programStatus = normalizeTag(stringField(candidate.program, "program_status") ?? "");
|
|
7912
|
-
if (readBoolean(candidate.program.is_active) === false || programStatus === "paused") {
|
|
8543
|
+
if (!filters.skip_status_check && (readBoolean(candidate.program.is_active) === false || programStatus === "paused")) {
|
|
7913
8544
|
return false;
|
|
7914
8545
|
}
|
|
7915
8546
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
@@ -8002,6 +8633,15 @@ function programCandidateMatches(candidate, filters) {
|
|
|
8002
8633
|
if (filters.min_added_7d !== undefined && (candidate.added7d ?? 0) < filters.min_added_7d) {
|
|
8003
8634
|
return false;
|
|
8004
8635
|
}
|
|
8636
|
+
if (filters.free_submission_only && !programHasNoSubmissionFee(candidate.program)) {
|
|
8637
|
+
return false;
|
|
8638
|
+
}
|
|
8639
|
+
if ((filters.competition_risks?.length ?? 0) > 0) {
|
|
8640
|
+
const risk = normalizeTag(stringField(candidate.program, "competition_risk") ?? "unknown");
|
|
8641
|
+
if (!filters.competition_risks.some((value) => normalizeTag(value) === risk)) {
|
|
8642
|
+
return false;
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8005
8645
|
return true;
|
|
8006
8646
|
}
|
|
8007
8647
|
function compareProgramCandidates(left, right, sortBy) {
|
|
@@ -8042,6 +8682,8 @@ function compareProgramCandidates(left, right, sortBy) {
|
|
|
8042
8682
|
right.lastUpdatedTime - left.lastUpdatedTime);
|
|
8043
8683
|
}
|
|
8044
8684
|
function candidateMatchesVdpMode(candidate, vdpMode) {
|
|
8685
|
+
if (vdpMode === "only")
|
|
8686
|
+
return stringField(candidate.program, "program_type") === "vdp";
|
|
8045
8687
|
if (vdpMode === "exclude") {
|
|
8046
8688
|
return !candidateLooksLikeVdp(candidate);
|
|
8047
8689
|
}
|
|
@@ -8270,7 +8912,7 @@ function registerPrompts(server, profile) {
|
|
|
8270
8912
|
tags: z.string().optional().describe("Optional comma-separated scope/language/target-type tags."),
|
|
8271
8913
|
max_programs: z.string().optional().describe("Optional maximum shortlist size.")
|
|
8272
8914
|
}
|
|
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.
|
|
8915
|
+
}, (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
8916
|
|
|
8275
8917
|
Find the best BBRadar program candidates.
|
|
8276
8918
|
${discoveryToolInstructions}
|
|
@@ -8321,8 +8963,12 @@ The plan must:
|
|
|
8321
8963
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
8322
8964
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
8323
8965
|
}
|
|
8324
|
-
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
8325
|
-
const normalizedFilters = stableStringify(
|
|
8966
|
+
function resolveFeedCursor(store, endpoint, filters, resume, config) {
|
|
8967
|
+
const normalizedFilters = stableStringify({
|
|
8968
|
+
api_base_url: config.apiBaseUrl,
|
|
8969
|
+
credential_scope: credentialCacheScope(config),
|
|
8970
|
+
filters: normalizeFeedCursorFilters(filters)
|
|
8971
|
+
});
|
|
8326
8972
|
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
8327
8973
|
const explicitAfterId = readNumber(filters.after_id);
|
|
8328
8974
|
if (explicitAfterId !== undefined) {
|
|
@@ -8353,7 +8999,7 @@ function advanceFeedCursor(store, cursor, rawMeta) {
|
|
|
8353
8999
|
stored_in_memory: true
|
|
8354
9000
|
};
|
|
8355
9001
|
}
|
|
8356
|
-
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, fetchPage, refreshInventory) {
|
|
9002
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, initialPage, fetchPage, refreshInventory) {
|
|
8357
9003
|
const startedAt = Date.now();
|
|
8358
9004
|
const results = [];
|
|
8359
9005
|
const sourceRequests = [];
|
|
@@ -8370,7 +9016,7 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8370
9016
|
do {
|
|
8371
9017
|
let api;
|
|
8372
9018
|
try {
|
|
8373
|
-
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot);
|
|
9019
|
+
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot, activeSnapshot ? initialPage + pagesFetched : 1);
|
|
8374
9020
|
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8375
9021
|
}
|
|
8376
9022
|
catch (error) {
|
|
@@ -8466,6 +9112,10 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
8466
9112
|
}
|
|
8467
9113
|
const data = readObject(api.data);
|
|
8468
9114
|
const returnedSnapshot = readString(readObject(data?.meta)?.snapshot);
|
|
9115
|
+
if (activeSnapshot && !returnedSnapshot)
|
|
9116
|
+
throw new Error("Stable snapshot response omitted its token; discard this page and start a new snapshot.");
|
|
9117
|
+
if (activeSnapshot && activeSnapshot !== "start" && returnedSnapshot !== activeSnapshot)
|
|
9118
|
+
throw new Error("Stable snapshot changed between pages; discard these pages and start a new snapshot.");
|
|
8469
9119
|
if (activeSnapshot === "start" && returnedSnapshot) {
|
|
8470
9120
|
activeSnapshot = returnedSnapshot;
|
|
8471
9121
|
}
|
|
@@ -8668,21 +9318,97 @@ function withApiMetadata(api, payload) {
|
|
|
8668
9318
|
hit: api.cached,
|
|
8669
9319
|
stale_while_revalidate: api.stale,
|
|
8670
9320
|
coalesced_live_request: api.coalesced,
|
|
8671
|
-
expires_at: api.cacheExpiresAt
|
|
9321
|
+
expires_at: api.cacheExpiresAt,
|
|
9322
|
+
no_store: api.noStore
|
|
8672
9323
|
}),
|
|
8673
9324
|
fetched_at: api.fetchedAt,
|
|
8674
9325
|
...payload
|
|
8675
9326
|
});
|
|
8676
9327
|
}
|
|
9328
|
+
function proPage(api, field) {
|
|
9329
|
+
const raw = readObject(api.data);
|
|
9330
|
+
if (!raw || !(field === "activity_summary" ? readObject(raw[field]) : Array.isArray(raw[field]))) {
|
|
9331
|
+
throw new Error(`BBRadar Pro API returned a malformed page: expected ${field}.`);
|
|
9332
|
+
}
|
|
9333
|
+
const data = sanitizeProPayload(api.data);
|
|
9334
|
+
return { ...data, ...withApiMetadata(api, {}) };
|
|
9335
|
+
}
|
|
9336
|
+
function assertProFeature(client, endpoint, flag) {
|
|
9337
|
+
const state = client.capabilitySnapshot();
|
|
9338
|
+
if (state.status !== "negotiated")
|
|
9339
|
+
return;
|
|
9340
|
+
const capability = client.endpointCapability(endpoint);
|
|
9341
|
+
if (!capability)
|
|
9342
|
+
throw new Error(`The negotiated BBRadar API does not advertise ${endpoint}. Upgrade the API deployment to use this tool.`);
|
|
9343
|
+
if (flag && capability[flag] === false) {
|
|
9344
|
+
throw new Error(`${endpoint} is advertised but ${flag}=false on this API deployment.`);
|
|
9345
|
+
}
|
|
9346
|
+
}
|
|
9347
|
+
function assertActivityTabInputs(tab, args) {
|
|
9348
|
+
const allowed = new Set([...ACTIVITY_SHARED_PARAMS, ...ACTIVITY_TAB_PARAMS[tab]]);
|
|
9349
|
+
const misplaced = Object.entries(args)
|
|
9350
|
+
.filter(([key, value]) => !allowed.has(key) && value !== undefined && value !== false)
|
|
9351
|
+
.map(([key]) => key);
|
|
9352
|
+
if (misplaced.length > 0) {
|
|
9353
|
+
throw new Error(`${misplaced.join(", ")} ${misplaced.length === 1 ? "does" : "do"} not apply to tab=${tab}. Remove ${misplaced.length === 1 ? "it" : "them"} rather than querying a different ranking.`);
|
|
9354
|
+
}
|
|
9355
|
+
if (typeof args.period === "string" && !ACTIVITY_COUNT_TABS.has(tab) && !ACTIVITY_EVENT_PERIODS.includes(args.period)) {
|
|
9356
|
+
throw new Error(`tab=${tab} uses rolling 7d, 30d or 90d periods.`);
|
|
9357
|
+
}
|
|
9358
|
+
}
|
|
9359
|
+
/** One ranking page without the capabilities document the API repeats on every page. */
|
|
9360
|
+
function activityRankingPage(api, includeMap) {
|
|
9361
|
+
const page = proPage(api, "rows");
|
|
9362
|
+
const capabilities = readObject(page.capabilities);
|
|
9363
|
+
delete page.capabilities;
|
|
9364
|
+
// With no enabled default view the API answers sort=null and no rows; keep
|
|
9365
|
+
// only this tab's views so the caller can choose an explicit one.
|
|
9366
|
+
if (page.sort === null && readArray(page.rows).length === 0 && capabilities) {
|
|
9367
|
+
const views = readArray(readArray(capabilities.tabs).map(readObject).find((tab) => tab?.id === page.tab)?.views);
|
|
9368
|
+
page.available_views = views;
|
|
9369
|
+
}
|
|
9370
|
+
return omitMapPoints(page, "points", includeMap);
|
|
9371
|
+
}
|
|
9372
|
+
function omitMapPoints(payload, field, include) {
|
|
9373
|
+
if (include || !Array.isArray(payload[field]))
|
|
9374
|
+
return payload;
|
|
9375
|
+
const { [field]: points, ...rest } = payload;
|
|
9376
|
+
return { ...rest, map_points_omitted: points.length };
|
|
9377
|
+
}
|
|
9378
|
+
function assertActivitySnapshotSelector(client, endpoint, snapshot) {
|
|
9379
|
+
// Older deployments ignore unknown parameters silently; never let a binding
|
|
9380
|
+
// the caller relies on be dropped without saying so.
|
|
9381
|
+
if (snapshot === undefined || client.capabilitySnapshot().status !== "negotiated")
|
|
9382
|
+
return;
|
|
9383
|
+
if (client.endpointCapability(endpoint)?.snapshot_parameter !== "snapshot") {
|
|
9384
|
+
throw new Error(`The negotiated BBRadar API does not advertise Activity version binding for ${endpoint}. Omit snapshot or upgrade the API deployment.`);
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9387
|
+
function assertActivityTierFilter(client, tiers) {
|
|
9388
|
+
// An API without the filter answers 422; say why locally and send nothing.
|
|
9389
|
+
if (!Array.isArray(tiers) || tiers.length === 0 || client.capabilitySnapshot().status !== "negotiated")
|
|
9390
|
+
return;
|
|
9391
|
+
if (!Array.isArray(client.endpointCapability("activity_rankings")?.opportunity_tiers)) {
|
|
9392
|
+
throw new Error("The negotiated BBRadar API does not advertise Activity opportunity tier filtering. Omit opportunity_tiers or upgrade the API deployment.");
|
|
9393
|
+
}
|
|
9394
|
+
}
|
|
9395
|
+
function assertHistoryCapture(client) {
|
|
9396
|
+
if (client.capabilitySnapshot().status === "negotiated" &&
|
|
9397
|
+
client.endpointCapability("history_changes")?.history_capture_enabled === false) {
|
|
9398
|
+
throw new Error("History endpoints are advertised but history_capture_enabled=false on this API deployment.");
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
8677
9401
|
function apiSourceMetadata(api) {
|
|
8678
9402
|
return stripUndefined({
|
|
8679
9403
|
cache_hit: api.cached,
|
|
8680
9404
|
cache_expires_at: api.cacheExpiresAt,
|
|
8681
|
-
coalesced_live_request: api.coalesced
|
|
9405
|
+
coalesced_live_request: api.coalesced,
|
|
9406
|
+
no_store: api.noStore
|
|
8682
9407
|
});
|
|
8683
9408
|
}
|
|
8684
9409
|
function errorPayload(error, decision, toolName, durationMs) {
|
|
8685
9410
|
if (error instanceof BBRadarApiError) {
|
|
9411
|
+
const recovery = error.status === 410 ? recoveryForGone(error, toolName) : undefined;
|
|
8686
9412
|
return stripUndefined({
|
|
8687
9413
|
request_id: error.requestId,
|
|
8688
9414
|
upstream_request_id: error.upstreamRequestId,
|
|
@@ -8692,6 +9418,7 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8692
9418
|
detail: sanitizeJson(error.detail),
|
|
8693
9419
|
errors: sanitizeJson(error.errors)
|
|
8694
9420
|
}),
|
|
9421
|
+
recovery,
|
|
8695
9422
|
mcp_rate_limit: toRateLimitPayload(decision),
|
|
8696
9423
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8697
9424
|
});
|
|
@@ -8705,6 +9432,25 @@ function errorPayload(error, decision, toolName, durationMs) {
|
|
|
8705
9432
|
mcp_timing: toolTimingPayload(toolName, durationMs)
|
|
8706
9433
|
};
|
|
8707
9434
|
}
|
|
9435
|
+
function recoveryForGone(error, toolName) {
|
|
9436
|
+
if (toolName === "get_activity_change_detail") {
|
|
9437
|
+
return { action: "refresh_activity_source", message: "Rerun the originating Activity changes or status query without its cursor, keeping its filters. Use the new meta.snapshot_id and event ID for this detail request; do not use snapshot=start." };
|
|
9438
|
+
}
|
|
9439
|
+
if (toolName.startsWith("get_activity_") || toolName === "get_program_payouts") {
|
|
9440
|
+
return { action: "restart_activity_query", message: "Restart this Activity query without its expired cursor or snapshot binding, keeping the same explicit period, basis, precision, sort, direction and filters; call get_activity_snapshot for the current version if binding is needed. Do not combine old pages or pass snapshot=start or after_id." };
|
|
9441
|
+
}
|
|
9442
|
+
const detail = typeof error.detail === "string" ? error.detail.toLowerCase() : error.message.toLowerCase();
|
|
9443
|
+
if (detail.includes("retained analytics") || detail.includes("precede")) {
|
|
9444
|
+
return { action: "choose_supported_interval", message: "Choose dates inside the retained analytics window." };
|
|
9445
|
+
}
|
|
9446
|
+
if (detail.includes("snapshot")) {
|
|
9447
|
+
return { action: "start_new_snapshot", message: "Restart on page 1 with snapshot=start; do not combine pages from the old snapshot." };
|
|
9448
|
+
}
|
|
9449
|
+
if (toolName.startsWith("get_history") || toolName === "get_program_history" || toolName.startsWith("get_analytics")) {
|
|
9450
|
+
return { action: "restart_query", message: "Restart this History or analytics query without its expired opaque cursor, keeping the intended filters." };
|
|
9451
|
+
}
|
|
9452
|
+
return { action: "reconcile_inventory", message: "The retained numeric feed has a gap; reconcile current inventory before seeding a new after_id." };
|
|
9453
|
+
}
|
|
8708
9454
|
function recordToolMetric(toolName, durationMs, status) {
|
|
8709
9455
|
const existing = TOOL_METRICS.get(toolName) ??
|
|
8710
9456
|
{
|
|
@@ -8823,15 +9569,19 @@ function compactTarget(target) {
|
|
|
8823
9569
|
identifier: targetIdentifier(target),
|
|
8824
9570
|
target_type: stringField(target, "target_type"),
|
|
8825
9571
|
in_scope: readBoolean(target.in_scope),
|
|
8826
|
-
eligible_for_bounty:
|
|
9572
|
+
eligible_for_bounty: target.eligible_for_bounty,
|
|
8827
9573
|
wildcard: readBoolean(target.wildcard),
|
|
8828
9574
|
path_kind: stringField(target, "path_kind"),
|
|
8829
9575
|
path_prefix: stringField(target, "path_prefix"),
|
|
8830
9576
|
match_quality: stringField(target, "match_quality"),
|
|
8831
9577
|
match_reason: stringField(target, "match_reason"),
|
|
8832
|
-
target_opportunity_score:
|
|
9578
|
+
target_opportunity_score: target.target_opportunity_score,
|
|
8833
9579
|
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
9580
|
+
score_version: target.score_version,
|
|
9581
|
+
evidence_coverage: target.evidence_coverage,
|
|
9582
|
+
target_score_breakdown: target.target_score_breakdown,
|
|
8834
9583
|
competition_risk: stringField(target, "competition_risk"),
|
|
9584
|
+
competition_risk_reason: stringField(target, "competition_risk_reason"),
|
|
8835
9585
|
why_this_target: stringField(target, "why_this_target")
|
|
8836
9586
|
});
|
|
8837
9587
|
}
|